agentleFS
aiappsgbbaiappsgbb/awesome-gbb/skills/foundry-cost-monitoring/SKILL.md

foundry-cost-monitoring

Skill8 starsChanged 12 days agoMITView on GitHub

Continuous Foundry token-cost monitoring, chargeback, budget alerts, and FinOps automation. Joins gen_ai.usage spans emitted by foundry-observability with the Azure Retail Prices API to compute per-agent / per-project / per-tenant cost projection. Wires Cost Management budgets + Action Groups, and uses Foundry's project-tag attribution (preview) for chargeback. USE FOR: foundry cost monitoring, token cost dashboard, FinOps Foundry, project-level chargeback, project tag cost attribution, Azure retail prices API, gen_ai.usage cost KQL, budget alert Foundry, Action Group cost webhook, token cost anomaly detection, OTel vs Cost Management reconciliation, per-agent / per-tenant token spend. DO NOT USE FOR: one-shot PTU vs PAYGO sizing (use paygo-ptu-cost-analyzer); emitting cost telemetry from agent code (use foundry-observability); gateway-only chargeback via x-app-id (use citadel-spoke-onboarding); fine-tuning cost estimates.

We split this skill into the rules it gives an agent

4 rules. About 0 are common, 0 are distinctive, and 4 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈0

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 4

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    No auth header. No subscription.

    line 73
  2. Very specific:

    If traffic goes through the AI Citadel gateway (see § 5), the APIM fragment-policy stamps the tenant identifier into the OTel span as …

    line 211
  3. Very specific:

    When Foundry traffic is fronted by an APIM AI Gateway (the AI Citadel Hub topology), tenant identity arrives on the x-app-id request header …

    line 221
  4. Very specific:

    Daily: run § 4.1 KQL for previous day, compare to query.usage result for the same scope + day.

    line 418

≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Foundry Cost Monitoring

Continuous, FinOps-grade cost monitoring for Microsoft Foundry. Where foundry-observability emits the gen_ai.usage.* spans, this skill consumes them — joining them with the Azure Retail Prices API to project per-agent, per-project, and per-tenant cost in near-real time, then wiring Cost Management budgets and Action Groups so the FinOps team sees overruns within minutes instead of next-month's invoice.

Sibling skills. Pair with foundry-observability for the telemetry emit path, paygo-ptu-cost-analyzer for one-shot PTU sizing decisions, and citadel-spoke-onboarding when chargeback is gateway-mediated via APIM x-app-id headers.


§1 — Three layers of cost truth

Foundry cost lands in three places, with three different latencies and three different fidelities:

LayerSourceLatencyFidelityUse for
1. Foundry portal estimateai.azure.com → Operate → Overview / Build → Agent / Model Monitor≈ 2–5 minEstimate — not invoiceableNear-real-time engineering insight (per-agent rollup)
2. OTel real-time projectionApp Insights customDimensions['gen_ai.usage.*'] × Retail Prices rate card≈ 2–5 min ingest + your KQL windowEngineering projection — not invoiceablePer-tenant / per-agent / per-conversation projection, anomaly detection, dashboards
3. Cost Mgmt actualsMicrosoft.CostManagement/query REST + invoice CSV exports≈ 8–24 hSource of truth for billingFinance reconciliation, chargeback, budgets

Reconciliation rule (and this is the rule that prevents the awkward quarterly call with finance): treat layer 3 as authoritative for anything that ends up on the bill. Layers 1 and 2 are engineering tools to see overruns before layer 3 catches up. Per the MS Learn cost guidance: "treat your invoice and meter records as the source of truth."


§2 — Pricing source: Azure Retail Prices REST API

The Retail Prices API is anonymous, public, and rate-card complete:

GET https://prices.azure.com/api/retail/prices
    ?$filter=<OData filter>
    [&$top=N]
    [&$skip=N]

No auth header. No subscription. Just curl (or urllib.request). The filter shape that matters for Foundry models in Sweden Central:

# Foundry models (post-rename — current GA service classification)
curl -s "https://prices.azure.com/api/retail/prices?\$filter=serviceName eq 'Foundry Models' and armRegionName eq 'swedencentral' and contains(meterName, 'Tokens')" | jq '.Items[0:3]'

# Embeddings — Sweden Central requires GlobalStandard SKU; filter accordingly
curl -s "https://prices.azure.com/api/retail/prices?\$filter=serviceName eq 'Foundry Models' and armRegionName eq 'swedencentral' and contains(productName, 'embedding')"

Each Items[] row carries the fields you need to price a token row:

FieldExampleWhat it means
serviceNameFoundry ModelsTop-level service classification
productNameAzure OpenAI, Azure Llama Models, Azure OpenAI GPT5Model family bucket
skuNamegpt 4.1 Inp regnlDeployment-shape SKU
meterNamegpt 4.1 Inp regnl TokensBilling meter
retailPrice0.00242Per unitOfMeasure
unitOfMeasure1K, 1MToken unit (mix of 1K and 1M — normalize before multiplying)
currencyCodeUSDPricing currency
armRegionNameswedencentralRegion
typeConsumptionConsumption for PAYGO, Reservation for PTU

Pagination: response includes NextPageLink; iterate until null when caching the full catalog (~thousands of rows for AI + Machine Learning).

Service-name drift. The Retail Prices API used to surface OpenAI meters under serviceName eq 'Cognitive Services'. Current GA classification is Foundry Models. The drift detector for this skill polls the Retail Prices endpoint weekly — if a new classification appears, refresh § 11 of this skill and bump PATCH.

Cache TTL recommendation: 24 h. Rates change weekly at most; daily refresh comfortably beats invoice cadence.


§3 — Foundry project tag chargeback (Preview)

Foundry automatically tags Models-sold-by-Azure usage records with a project tag whose value is the Foundry project name. The chargeback flow is then:

  1. In Cost Management → Cost Analysis, scope to your Foundry resource.
  2. Add filter → Tag → project.
  3. Pick one or more projects → cost split by project, time-series.

CLI equivalent:

az consumption usage list \
  --subscription "$SUB" \
  --start-date "$(date -u -v-30d +%Y-%m-%d)" \
  --end-date "$(date -u +%Y-%m-%d)" \
  --query "[?tags.project=='proj-fsi-claims'].{date:usageStart, meter:meterDetails.meterName, qty:quantity, cost:pretaxCost}" \
  -o table

Verbatim preview limit (MS Learn, manage-costs#chargeback-with-project-level-cost-attribution-preview):

"Project-level cost attribution is currently supported for Models sold by Azure (Azure Direct models, including Azure OpenAI). It isn't yet supported for models served through Azure Marketplace."

Concretely: Azure OpenAI / Foundry-native models → tagged automatically. Llama / Mistral / DeepSeek / Cohere via Azure Marketplace → no project tag; fall back to layer-2 OTel projection (§ 4) or per-resource grouping (one project ↔ one resource group).

The tag is not manually applied — do not try to add it via az tag create-or-update; Foundry control plane writes it on the underlying meter records.


§4 — In-flight cost projection: KQL on gen_ai.usage.*

This is the engineering-insight layer. Requires foundry-observability already wired (account-level App Insights connection + ACA-side configure_azure_monitor()). The MAF 1.6.0+ schema emits OTel spans with the following customDimensions:

KeyTypeExample
gen_ai.systemstringazure_openai
gen_ai.request.modelstringgpt-5.4-mini
gen_ai.response.modelstringgpt-5.4-mini-2026-01-15
gen_ai.usage.input_tokensint1284
gen_ai.usage.output_tokensint342
gen_ai.operation.namestringchat, embeddings

Spans land in traces / dependencies depending on instrumentation. The canonical filter for MAF-emitted spans is cloud_RoleName == "agent_framework" (verified at MAF 1.6.0+ — see foundry-observability § "OTel cloud_RoleName" for the rationale).

4.1 Per-agent cost rollup (last 24 h)

// Per-agent token cost (last 24h) — projection-only, not invoiceable
let rateCard = externaldata(model:string, in_rate_usd_per_1k:real, out_rate_usd_per_1k:real)
  [@"https://raw.githubusercontent.com/<your-org>/<your-repo>/main/rate-card.csv"]
  with (format="csv", ignoreFirstRecord=true);
dependencies
| where timestamp > ago(24h)
| where cloud_RoleName == "agent_framework"
| where isnotempty(customDimensions["gen_ai.usage.input_tokens"])
| extend
    agent      = tostring(customDimensions["gen_ai.agent.name"]),
    model      = tostring(customDimensions["gen_ai.request.model"]),
    in_tokens  = toint(customDimensions["gen_ai.usage.input_tokens"]),
    out_tokens = toint(customDimensions["gen_ai.usage.output_tokens"])
| join kind=leftouter rateCard on model
| extend
    in_cost  = (in_tokens  / 1000.0) * in_rate_usd_per_1k,
    out_cost = (out_tokens / 1000.0) * out_rate_usd_per_1k
| summarize
    calls            = count(),
    input_tokens     = sum(in_tokens),
    output_tokens    = sum(out_tokens),
    projected_cost_usd = sum(in_cost + out_cost)
    by agent, model
| order by projected_cost_usd desc

4.2 Per-project cost rollup

gen_ai.agent.name carries the agent name. To get per-project rollup without the preview tag, instrument the project name as a resource attribute on the OTel exporter (e.g. OTEL_RESOURCE_ATTRIBUTES="foundry.project=proj-fsi-claims") and group on customDimensions["foundry.project"] instead of agent. Where the project tag (§ 3) IS available, prefer Cost Management for authoritative reconciliation and reserve KQL for in-flight projection.

4.3 Per-tenant cost rollup (Citadel APIM gateway path)

If traffic goes through the AI Citadel gateway (see § 5), the APIM fragment-policy stamps the tenant identifier into the OTel span as enduser.id (gateway maps x-app-id header → enduser.id). The per-tenant rollup is identical to § 4.1 but groups on tostring(customDimensions["enduser.id"]).


§5 — Citadel APIM x-app-id chargeback path

When Foundry traffic is fronted by an APIM AI Gateway (the AI Citadel Hub topology), tenant identity arrives on the x-app-id request header and the gateway logs it into App Insights. This is the cleanest chargeback path for multi-tenant SaaS deployments because it does not depend on Foundry's project-tag preview.

Wiring path (full detail in citadel-spoke-onboarding):

Tenant request → APIM (validates JWT, stamps x-app-id) → Foundry
                                                            ↓
                                                    OTel span tagged with
                                                    enduser.id = x-app-id

KQL aggregates over enduser.id then rejoin to the customer-ID-to-name table (kept outside this skill — usually a Cosmos/Kusto reference table).


§6 — Budget alerts via Azure Cost Management

Set bottom-up: subscription budget + per-RG budgets for spike isolation.

6.1 Bicep (recommended)

// budget.bicep — per-RG budget with notifications at 80% / 100% / 120%
@description('Budget amount in USD per month')
param budgetAmount int = 5000

@description('FinOps distribution list')
param finopsEmails array = [
  'finops@<example-bank>.com'
]

resource budget 'Microsoft.Consumption/budgets@2024-08-01' = {
  name: 'budget-foundry-${resourceGroup().name}'
  properties: {
    amount: budgetAmount
    timeGrain: 'Monthly'
    timePeriod: {
      startDate: '2026-06-01'
    }
    category: 'Cost'
    notifications: {
      Warning_80: {
        enabled: true
        operator: 'GreaterThan'
        threshold: 80
        contactEmails: finopsEmails
        thresholdType: 'Actual'
      }
      Critical_100: {
        enabled: true
        operator: 'GreaterThan'
        threshold: 100
        contactEmails: finopsEmails
        contactGroups: [
          actionGroup.id
        ]
        thresholdType: 'Actual'
      }
      Forecast_120: {
        enabled: true
        operator: 'GreaterThan'
        threshold: 120
        contactEmails: finopsEmails
        thresholdType: 'Forecasted'
      }
    }
    filter: {
      tags: {
        name: 'project'
        operator: 'In'
        values: [
          'proj-fsi-claims'
          'proj-fsi-onboarding'
        ]
      }
    }
  }
}

resource actionGroup 'Microsoft.Insights/actionGroups@2024-10-01-preview' = {
  name: 'ag-finops-${resourceGroup().name}'
  location: 'global'
  properties: {
    groupShortName: 'finops'
    enabled: true
    webhookReceivers: [
      {
        name: 'finops-teams'
        serviceUri: '<teams-incoming-webhook-url>'
        useCommonAlertSchema: true
      }
    ]
  }
}

6.2 CLI equivalent

# Resource-group scoped budget — fires Action Group on actual cost > $5000
az consumption budget create-with-rg \
  --resource-group "$RG" \
  --budget-name "budget-foundry-$RG" \
  --amount 5000 \
  --time-grain Monthly \
  --start-date "$(date -u +%Y-%m-01)" \
  --category Cost \
  --notifications-properties \
      "[{operator:'GreaterThan',threshold:80,contactEmails:['finops@<example-bank>.com'],thresholdType:'Actual'}]"

Hard-limit caveat (MS Learn manage-costs § Create budgets): "Azure OpenAI doesn't currently provide [hard-limit] functionality. You can start automation from action groups as part of your budget notifications to take more advanced actions, but this functionality requires additional custom development."

Budgets are alerts, not throttles. For hard ceilings, wire the Action Group → Logic App / Function that disables the offending deployment (e.g. az cognitiveservices account deployment update --properties.callRateLimit=0).


§7 — Action Group → Webhook / Logic App / Teams

Standard Azure pattern. The Action Group from § 6.1 wires three common downstream channels:

ChannelUse case
Teams incoming webhookFinOps channel notification (useCommonAlertSchema: true so payload is parseable)
Logic AppDisable / scale-down offending deployments automatically
Azure FunctionCustom remediation (e.g. notify owner, file ticket, page on-call)

The Action Group is fire-and-forget — Cost Management posts the budget breach event payload to every receiver in parallel.


§8 — Anomaly detection on token cost

The KQL queries in § 4 are point-in-time. To catch unexpected spend spikes (a leaked API key, a runaway agent loop, a prompt that exploded context-window utilisation), wrap the time-series in series_decompose_anomalies:

// 7-day token-cost anomaly detection per agent
let bin_size = 1h;
dependencies
| where timestamp > ago(7d)
| where cloud_RoleName == "agent_framework"
| where isnotempty(customDimensions["gen_ai.usage.input_tokens"])
| extend
    agent      = tostring(customDimensions["gen_ai.agent.name"]),
    model      = tostring(customDimensions["gen_ai.request.model"]),
    in_tokens  = toint(customDimensions["gen_ai.usage.input_tokens"]),
    out_tokens = toint(customDimensions["gen_ai.usage.output_tokens"])
| summarize total_tokens = sum(in_tokens + out_tokens) by agent, bin(timestamp, bin_size)
| make-series series=sum(total_tokens) default=0 on timestamp from ago(7d) to now() step bin_size by agent
| extend (anomalies, score, baseline) = series_decompose_anomalies(series, 2.5)
| mv-expand timestamp, series, anomalies, score, baseline
| where toint(anomalies) != 0
| project agent, timestamp=todatetime(timestamp), tokens=toint(series), anomaly=toint(anomalies), score=todouble(score)
| order by score desc

Wire this query to an Azure Monitor scheduled query alert (15-min frequency, threshold count() > 0) → same Action Group from § 6.


§9 — Reconciliation drift: OTel projection vs Cost Mgmt actuals

The two layers will never match exactly. Expected drift:

SourceLatencyCauses of drift vs invoice
OTel projection2–5 min ingestStale rate card; dropped spans; OTel sampling; cached input pricing not modeled; reservation discounts not applied
Cost Mgmt actuals8–24 hAuthoritative — the bill

Acceptable drift: ±5% day-over-day, ±2% month-over-month. If your projection consistently overshoots, the rate card is stale (refresh from Retail Prices API). If it consistently undershoots, you're missing a meter — usually the cached-input meter (some models split cached input into a separate, cheaper meter) or the fine-tuned-hosting hourly meter.

Reconciliation cadence:

  1. Daily: run § 4.1 KQL for previous day, compare to query.usage result for the same scope + day. Log delta.
  2. Weekly: refresh the rate-card CSV from Retail Prices.
  3. Monthly: full reconciliation against invoice CSV export. Adjust series_decompose_anomalies baseline if drift bias exceeds ±2%.

§10 — Common pitfalls

PitfallSymptomFix
Model-name normalizationKQL model join misses (gpt-5.4-mini vs gpt-5.4-mini-2026-01-15)Strip the date suffix before joining: extend model_base = extract(@"^([a-z0-9\-\.]+?)(?:-\d{4}-\d{2}-\d{2})?$", 1, model)
Missing meter for new SKURecently-released model returns no rateRefresh rate card (Retail Prices is updated within hours); fall back to a same-family proxy SKU temporarily
Project-tag preview limitMarketplace models (Llama, Mistral, DeepSeek) return zero rows when filtered by tags.project§ 3 verbatim — fall back to OTel customDimensions["foundry.project"]
OTel ingestion lagCost dashboard shows zero for the last 5 minExpected. Use ago(15m) minimum window. Pattern 13 (LAW soft-PASS) for any alert
Marketplace-vs-direct boundaryReconciliation drift > 10% on a Marketplace-heavy tenantMarketplace meters bill under different scopes (Global resources resource group); scope KQL accordingly
Unit-of-measure mixCost projection 1000× too highRate card has both 1K and 1M units — normalize to 1K in your rate-card cache
Cached input pricingProjection consistently overshoots actualsSome models emit gen_ai.usage.input_tokens.cached separately; price that with the cheaper cached-input meter
Currency mixMulti-region tenant has both USD and EUR rowsFilter currencyCode eq 'USD' (or normalize FX) in your rate-card cache
Cost Mgmt 429query.usage throttled under loadCache to a per-day Kusto table; never query Cost Mgmt from a per-request hot path

§11 — Permissions matrix

Per MS Learn manage-costs#configure-permissions-to-view-costs:

RoleScopeWhat it grantsRequired for
Cost Management Reader (built-in)Subscription or RGView costs and usage data via Cost Mgmt§ 1 layer 3, § 3, § 6, § 9
Foundry User (built-in, formerly Azure AI User)Foundry account / projectView Foundry resource data and usage context§ 1 layer 1 (portal estimates), per-agent Monitor tab
Monitoring Reader (built-in)Subscription or RG containing App InsightsRead App Insights metrics + alerts metadata§ 7 Action Group inspection
Log Analytics Reader (built-in)LAW workspaceRun KQL via query_workspace§ 4, § 8
Application Insights Component Contributor (built-in)App Insights resourceManage alerts (create scheduled-query rules)§ 8 alert wiring
Foundry Cost Reader (custom — example below)Foundry resourceLeast-privilege read for cost-only dashboardsOptional — when you don't want to grant full Cost Management Reader

MS Learn role rename notice (current as of 2026-06). "The Foundry RBAC roles were recently renamed. Foundry User, Foundry Owner, Foundry Account Owner, and Foundry Project Manager were previously named Azure AI User, Azure AI Owner, Azure AI Account Owner, and Azure AI Project Manager. … The role IDs and core permissions are unchanged."

Example custom role: Foundry Cost Reader

MS Learn ships an example JSON for a least-privilege custom role covering cost-only views. Use this when full Cost Management Reader is too broad:

{
  "Name": "Foundry Cost Reader",
  "IsCustom": true,
  "Description": "Can see cost metrics in Foundry",
  "Actions": [
    "Microsoft.Consumption/*/read",
    "Microsoft.CostManagement/*/read",
    "Microsoft.Resources/subscriptions/read",
    "Microsoft.CognitiveServices/accounts/AIServices/usage/read"
  ],
  "NotActions": [],
  "DataActions": [],
  "NotDataActions": [],
  "AssignableScopes": [
    "/subscriptions/<subscriptionId>/resourceGroups/<resourceGroupName>/providers/Microsoft.CognitiveServices/accounts/<foundryResourceName>"
  ]
}

Create with:

az role definition create --role-definition foundry-cost-reader.json

Important — the custom role on its own does NOT grant Foundry resource visibility. Pair it with Foundry User when the role recipient also needs to inspect Foundry agents / models / projects.

CI-side grants used by awesome-gbb's own fixture

The CI UAMI currently has Contributor + AcrPush + Cognitive Services OpenAI User + Foundry User on the CI resource group (see AGENTS.md § 9.7 and .github/ci-shared-preamble.md for the canonical identifiers). The fixture for this skill exercises the Cost Management read path; the role needed is Cost Management Reader at subscription scope:

az role assignment create \
  --assignee-object-id "$AZURE_OBJECT_ID" \
  --assignee-principal-type ServicePrincipal \
  --role "Cost Management Reader" \
  --scope "/subscriptions/$AZURE_SUBSCRIPTION_ID"

The fixture soft-passes on 403 from the Cost Management REST call per AGENTS.md § 9.7 Pattern 25, so the CI run still emits PASS when this grant is pending — the role grant is tracked separately and does not block the catalog from shipping.


§12 — Inference cost optimization levers (FinOps)

§ 1–§ 9 tell you what you spend; these levers cut it. Inference cost turns on three knobs distinct from generic compute right-sizing: token volume, deployment type (Standard pay-per-token vs Provisioned/PTU), and deployment SKU/region. Apply in priority order:

PriorityLeverDetectionActionSavings
🔴 CritIdle PTU deploymentsku.name Provisioned + sustained util below breakevenPTU bills 24/7 — cut count or move to GlobalStandard30–70%
🔴 CritFlagship on simple tasksgpt-5.4 for classify/extract/route at scalemove to -mini/-nano50–95%
🟠 HighSync endpoint for batchable workoffline evals/backfills on real-time endpointBatch API (24h window)~50%
🟠 HighUncached repeated contextlarge static system prompt resent every callenable prompt caching10–40%
🟠 HighRegional SKU where Global fitsStandard/DataZoneStandard, no residency needswitch to GlobalStandard5–30%
🟡 MedUnbounded outputno max_completion_tokens capcap output (priced > input)10–30%
🟡 MedOversized embeddingtext-embedding-3-large where small fitstext-embedding-3-small60–80%

Deployment-type breakeven: PTU only beats Standard once sustained throughput clears the per-token breakeven — model it from real volume (paygo-ptu-cost-analyzer) before reserving. Below breakeven, idle PTU is the single largest inference waste. Per-app showback needs token-level attribution Cost Management lacks — emit gen_ai.usage.* tagged by consumer (§ 4) and reconcile against deployment cost (§ 9).


See also

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.