🌐 Live app: https://aicostcalculator.livethoughts.net (also reachable at the default Azure Container Apps hostname: https://ca-aicost-web.grayisland-f9592069.swedencentral.azurecontainerapps.io)
A .NET Aspire application that estimates monthly Azure cost for enterprise-grade AI reference architectures — from business metrics, not SKUs.
Users pick a use case template (e.g. Enterprise Chatbot with RAG, Document Intelligence Pipeline), configure business inputs like "daily active users" or "documents per month", and receive a monthly cost estimate broken down by Azure resource, using live prices from the Azure Retail Prices API.
Each use case template is a single YAML file describing:
- The business metrics the user configures (units, min/max/step).
- The advanced control assumptions that translate business metrics into Azure consumption (tokens per interaction, replicas per user cohort, ingestion ratios, ...).
- The Azure resources in the architecture, each with an Azure Retail Prices API filter and an NCalc expression that turns the metrics + variables into a monthly billable quantity.
Add or edit templates in templates/*.yaml, restart the API, and new
architectures appear in the gallery. See
Authoring a use case template for the full
walkthrough or CONTRIBUTING.md for the PR workflow.
| Template | Category | Primary services |
|---|---|---|
| Enterprise Chatbot with RAG | Conversational AI | Azure OpenAI, AI Search |
| Document Intelligence Pipeline | Document AI | Document Intelligence, OpenAI |
| Call Center Analytics | Speech / Analytics | Speech, OpenAI, Storage |
| AI Code Assistant | Developer Productivity | Azure OpenAI, APIM |
| Multimodal Image Analysis | Vision | AI Vision, OpenAI |
Missing an architecture? Open an issue or send a PR following the template authoring guide.
- Gallery (
/): filter by industry, category, and tags; free-text search; each card links to the calculator. - Template detail (
/templates/{id}): configure business inputs, toggle between Basic (business only) and Advanced (business + assumption multipliers), pick an Azure region, see monthly cost + per-resource breakdown.
AICostCalculator/
├── AGENTS.md # spec2cloud orchestrator (unrelated to app runtime)
├── README.md
├── templates/ # YAML use case templates (edit here to add architectures)
│ ├── enterprise-chatbot-rag.yaml
│ ├── document-intelligence-pipeline.yaml
│ ├── call-center-analytics.yaml
│ ├── ai-code-assistant.yaml
│ └── multimodal-image-analysis.yaml
└── src/
├── AICostCalculator.AppHost/ # Aspire orchestrator
├── AICostCalculator.ServiceDefaults/ # OpenTelemetry, health checks, service discovery
├── AICostCalculator.Shared/ # DTOs shared by API + Web
├── AICostCalculator.ApiService/ # Minimal API: templates, prices, calculator
│ └── Services/
│ ├── TemplateLoader.cs
│ ├── AzureRetailPricesClient.cs
│ └── CostCalculator.cs
└── AICostCalculator.Web/ # Blazor Server frontend
└── Components/Pages/
├── Home.razor # Gallery + filters
└── TemplateDetail.razor # Basic/Advanced calculator
- .NET SDK 10.0 (also works with .NET 9 — retarget the
TargetFrameworkin every.csprojif you need to). - Aspire CLI 13.5.3 or newer (
dotnet tool install -g aspire.cli) OR justdotnet runon the AppHost. - Internet access from the API service to
https://prices.azure.com/api/retail/prices.
The repository ships a
src/Directory.Build.propsthat disables the MSBuild workload resolver. This project does not use any .NET workloads (Android, iOS, MAUI, WASM), and disabling the resolver avoids build hangs/failures on machines with a broken or partial workload set. Leave it in place unless you add workload-based projects.
cd src\AICostCalculator.AppHost
dotnet runThe Aspire dashboard prints a URL you can open in your browser
(https://localhost:<port>/login?t=...). From the dashboard you can open both
the webfrontend (the gallery) and apiservice (Swagger/OpenAPI) directly.
Or, with the Aspire CLI:
aspire runaspire runhangs at "Building AppHost" — your Aspire CLI is older than the AppHost SDK. Update it:dotnet tool update -g aspire.cli.SDK Resolver Failure … workload set … has missing manifests— a broken .NET workload set. This repo bypasses the workload resolver viasrc/Directory.Build.props; if you removed that file, either put it back or rundotnet workload repair(may require admin).- Dashboard opens but a resource is unhealthy — check the resource's Logs
tab in the Aspire dashboard for the actual error; the
/healthendpoint is wired on both services.
| Method | Route | Purpose |
|---|---|---|
| GET | /api/templates |
Summaries of all loaded templates |
| GET | /api/templates/{id} |
Full template incl. business metrics, advanced variables, resources, formulas |
| GET | /api/filters |
Distinct industries, categories, tags for the gallery filter |
| GET | /api/regions |
Supported Azure regions |
| POST | /api/calculate |
Compute cost — body: { templateId, region, currency, businessMetrics, advancedOverrides? } |
Every architecture in the gallery is a single YAML file in templates/. The
API loads every *.yaml file at startup (see
AICostCalculator.ApiService/Services/TemplateLoader.cs) — add a file,
restart the API, and it appears in the gallery. There is no code, no
migration, and no database.
This section is the end-to-end recipe for building a new template. Follow it in order.
Start from an official Microsoft reference or a well-understood pattern. You need to know, at minimum:
- Which Azure services participate.
- What "unit of demand" drives cost (users, requests, documents, minutes of audio, tokens, images, …).
- Any fixed-cost components (APIM tier, reserved instances, dedicated throughput units).
Good sources:
- Azure Architecture Center
- Azure OpenAI landing zones
- Existing templates in this repo — copy one and adapt it.
Copy-Item templates\enterprise-chatbot-rag.yaml templates\my-new-arch.yamlEvery field is documented inline in the existing files. Prefer editing a real
template over writing from scratch — it is far easier to keep formulas and
price_query filters correct.
id: my-new-arch # stable identifier — becomes the URL (/templates/my-new-arch)
name: My New Architecture # gallery card title
category: Conversational AI # groups templates in the gallery filter
short_description: One-liner shown on the gallery card.
long_description: |
Multi-paragraph rich description shown on the detail page.
Explain the pattern, when to use it, and what "cost driver" the user configures.
industries: [Financial Services, Retail]
tags: [RAG, GPT-4o]
architecture_center_url: https://learn.microsoft.com/azure/architecture/...
icon_url: null # optional, absolute URL to an SVG/PNGRules:
idmust be unique and URL-safe (lowercase, hyphens).category,industries, andtagsvalues drive the gallery filters — reuse existing labels when possible so filters stay tidy.
These are what an end user tunes on the Basic view. Keep them close to how the customer thinks about their workload — never expose SKU-level knobs here.
business_metrics:
- key: daily_active_users # identifier used in formulas
label: Daily active users # shown in the UI
description: Number of unique users interacting with the assistant per day.
unit: users/day
default_value: 500
min: 1
max: 1000000
step: 10Every key used in monthly_quantity_formula must be defined here (or in
advanced_variables).
These translate business metrics into Azure consumption. They show up only in the Advanced view.
advanced_variables:
- key: input_tokens_per_interaction
label: Input tokens per interaction
unit: tokens
default_value: 1500
- key: output_tokens_per_interaction
label: Output tokens per interaction
unit: tokens
default_value: 500Keep defaults defensible — link a source in description when the assumption
is non-obvious (e.g. "based on average 3-page RAG context + 400-word answer").
For each resource in the architecture, add one entry.
azure_resources:
- key: openai_input
name: Azure OpenAI GPT-4o (input tokens)
service: Azure OpenAI
price_query:
product_name: Azure OpenAI
meter_name: gpt 4o 1120 Inp regnl
monthly_quantity_formula: >
(daily_active_users * avg_interactions_per_user *
input_tokens_per_interaction * 22) / 1000
notes: Meter is priced per 1K tokens; formula divides by 1000.- Use one resource entry per meter you want to bill against. Input tokens and output tokens for the same model are usually two entries.
- Multiply by working days (
22) or calendar days (30) as appropriate for the workload.
Non-empty fields are AND-ed into the OData $filter sent to
https://prices.azure.com/api/retail/prices.
| Field | Match | Example |
|---|---|---|
service_name |
equals | Azure Container Apps |
arm_sku_name |
equals | (rarely needed) |
sku_name |
equals | Standard S1 |
meter_name |
contains | gpt 4o 1120 Inp regnl |
product_name |
contains | Azure OpenAI |
arm_region_name |
equals — usually left blank; region comes from the request | |
price_type |
equals — defaults to Consumption |
|
currency_code |
equals — defaults to USD from the request |
meter_name and product_name use contains(...) because Azure often
versions meters (gpt 4o 1120 Inp regnl Tokens). Supply a stable substring;
the client picks the first non-zero result.
If a price cannot be resolved, the resource shows unit price 0 and a warning
is added to the response — the calculator degrades gracefully so a stale meter
name never breaks the whole gallery.
An NCalc expression producing the monthly
billable quantity in the unit the Azure meter is quoted in. Every
identifier must be a declared business_metrics[].key or
advanced_variables[].key.
Supported operators: + - * / % and functions like Ceiling, Floor,
Max, Min, Round, Pow, Abs.
Examples:
# Tokens → 1K-token units:
monthly_quantity_formula: (users * interactions * tokens * 22) / 1000
# Users → whole-number replica count:
monthly_quantity_formula: Ceiling(daily_active_users / 250)
# Total requests → per-1M units:
monthly_quantity_formula: (requests_per_day * 30) / 1000000For services with a fixed monthly cost (e.g. an APIM tier), omit price_query
and set flat_monthly_cost:
- key: apim
name: API Management (Standard tier)
service: Azure API Management
flat_monthly_cost: 672.31
monthly_quantity_formula: "1"
notes: Flat monthly rate — no per-unit lookup.The meter names in price_query are the trickiest part. Two reliable ways to
discover them:
- Query the retail prices API directly. Example: all Azure OpenAI GPT-4o
input meters in East US:
Pick the shortest substring of
curl "https://prices.azure.com/api/retail/prices?`$filter=serviceName eq 'Azure OpenAI' and armRegionName eq 'eastus' and contains(meterName, 'gpt 4o') and contains(meterName, 'Inp')"meterNamethat uniquely identifies the meter you want and paste it intoprice_query.meter_name. - Run the API service locally and hit
GET /api/calculatefor your template. The response includes anywarningsabout resources whose meter did not resolve — iterate on the substrings until every resource has a non-zero unit price.
# Terminal 1 — run the app
cd src
aspire runThen in the Aspire dashboard:
- Open apiservice → Swagger UI.
GET /api/templates→ confirm your new template is listed.POST /api/calculatewith:{ "templateId": "my-new-arch", "region": "eastus", "currency": "USD", "businessMetrics": { "daily_active_users": 1000 } }- Verify
totalMonthlyCost > 0, no warnings, and every resource has a sensibleunitPriceandmonthlyQuantity. - Open webfrontend → your template shows up on the gallery, filters work, and the detail page renders both Basic and Advanced views.
Even if the formulas evaluate, the result must be credible.
- Sweep the sliders across their range and confirm the cost curves make sense (linear vs. step for replicas, no negative costs, no NaN).
- Compare a single mid-range configuration against the Azure pricing calculator for the same architecture. Aim for the same order of magnitude; if it is off by 10x, a formula unit is wrong (per-1K vs per-1M is the usual culprit).
- Toggle between Basic and Advanced — the total should not jump when you simply reveal the advanced view without changing values.
Open a pull request with:
- The new
templates/*.yamlfile. - A short PR description explaining the architecture, cost drivers, and reference link.
- A screenshot of the detail page and one sample calculation for reproducibility.
See CONTRIBUTING.md for the full checklist.
| Section | Field | Notes |
|---|---|---|
| root | id, name, category, short_description, long_description |
Required (except long_description) |
| root | industries, tags |
Lists — drive gallery filters |
| root | architecture_center_url, icon_url |
Optional |
business_metrics[] |
key, label, default_value, min, max, step, unit, description |
key MUST match variables used in formulas |
advanced_variables[] |
key, label, default_value, unit, description |
Shown only in Advanced view |
azure_resources[] |
key, name, service, description, price_query, monthly_quantity_formula, flat_monthly_cost, notes |
See above |
- The user submits
businessMetrics(and optionallyadvancedOverrides) for a template. - The
CostCalculatorbuilds an NCalc parameter dictionary from those values, falling back to each metric/variable'sdefault_valuewhen not supplied. - For every resource:
MonthlyQuantityFormulais evaluated → monthly quantity.- If
flat_monthly_costis set, that flat value is used as unit price and monthly cost. - Otherwise
AzureRetailPricesClientis queried, results are cached for 6 hours in-memory, and the first non-zero match becomes the unit price. monthly_cost = monthly_quantity × unit_price.
- All resource costs are summed into
TotalMonthlyCostand returned with a per-resource breakdown.
- Add a template → drop a new
templates/*.yamlfile, restart the API. - Add a region → edit the region list in
src/AICostCalculator.ApiService/Program.cs. - Change theme / brand → edit
src/AICostCalculator.Web/wwwroot/app.css. - Add a shared type → put it in
src/AICostCalculator.Shared/Models.cs; both API and Web pick it up automatically.
The Azure Retail Prices catalog changes over time. Meter names include model
versions (e.g. gpt 4o 1120), and some services have been reorganized under a
Foundry Tools service umbrella. The contains(meterName, ...) behaviour
tolerates version drift for a while, but for long-lived deployments you may need
to periodically update meter substrings in the templates.
The app is deployed to Azure Container Apps in the shared
cae-azinfrawork-shared environment (subscription
1f79b416-e33a-4be5-bcff-e1fa7e300776, resource group azureinfrawork,
Sweden Central):
- Two Container Apps:
ca-aicost-web(external ingress) andca-aicost-api(internal-only ingress). - Images live in the shared registry
crazinfraworkml76cbhfv5.azurecr.iounderaicostcalculator/webandaicostcalculator/apiservice. - Infrastructure as code in
infra/main.bicep. - CI/CD in
.github/workflows/deploy.yml— every push tomainbuilds new images withaz acr buildand rolls the Container Apps viaaz deployment group create, using OIDC login through the sharedid-azinfrawork-githubmanaged identity. - Custom domain
aicostcalculator.livethoughts.netbound with an Azure-managed certificate (auto-renewed).
Full runbook, DNS records, and rollback steps: docs/DEPLOYMENT.md.
Contributions are very welcome — especially new use case templates for the gallery. Please read CONTRIBUTING.md for the full workflow, coding conventions, and the checklist for submitting a new template.
All participation in this project is governed by the Code of Conduct.
This project is licensed under the MIT License.
Cost estimates are informational and use live public prices from the Azure Retail Prices API. They do not include Enterprise Agreement discounts, reservations, savings plans, or negotiated pricing, and they may lag Azure changes by hours or days. Always confirm final pricing with the Azure pricing calculator or your Microsoft account team before making commitments.
