Steg 9 (R4): unified migrate-corpus.mjs --write over engineering/governance/ infrastructure/security. 327 filer mutert, verified=null, prosa byte-identisk (fra første ## seksjon), advisor urørt (0 endringer). To applier-fixes oppdaget under kjøring (TDD, RED→GREEN): - insertHeaderFields: anker faller nå tilbake når en meta-linje selv passerer 500B (2 filer pakket et avsnitt i **Status:** → Type/Source landet utenfor scan-vinduet, applierens post-write-assertion fanget + restaurerte). - normalizeStaleVerified: fjerner nå ALLE stale non-date **Verified:** i 500B-vinduet, inkl. stray body-dup rett under --- (9 mlops-genaiops-filer var ellers falskt "verified"/fresh, droppet fra worklist). Operatør-godkjent utvidelse av carve-out; kun stray metadata-linjer, aldri prosa. test-transform-criterion: precondition oppdatert til post-migrasjons-sannhet (fila bærer nå Source). Suite 728/728 grønn.
461 lines
18 KiB
Markdown
461 lines
18 KiB
Markdown
# Cost Tracking & Chargeback via APIM Policies
|
|
|
|
**Last updated:** 2026-06-19
|
|
**Status:** GA
|
|
**Category:** API Management & AI Gateway
|
|
**Type:** reference
|
|
**Source:** https://learn.microsoft.com/azure/cost-management-billing/costs/overview-cost-management
|
|
|
|
---
|
|
|
|
## Innhold
|
|
|
|
- [Introduksjon](#introduksjon)
|
|
- [Token Counting from Responses](#token-counting-from-responses)
|
|
- [Model Routing Tracking](#model-routing-tracking)
|
|
- [Chargeback Tagging](#chargeback-tagging)
|
|
- [Azure Cost Management Integration](#azure-cost-management-integration)
|
|
- [Custom Metrics](#custom-metrics)
|
|
- [FinOps Integrasjon](#finops-integrasjon)
|
|
- [Referanser](#referanser)
|
|
- [For Cosmo](#for-cosmo)
|
|
|
|
## Introduksjon
|
|
|
|
Når organisasjoner skalerer sin bruk av Azure OpenAI og andre AI-tjenester, blir kostnadssynlighet og tildeling av kostnader til riktig avdeling, prosjekt eller team en kritisk utfordring. Azure API Management (APIM) fungerer som et naturlig punkt for å samle inn kostnadsdata fra AI-modeller gjennom policyer som fanger token-bruk, modell-informasjon og forbruker-identitet. Denne informasjonen kan deretter brukes for intern fakturering (chargeback) og kostnadsoptimalisering.
|
|
|
|
For norsk offentlig sektor med stramme budsjetter og krav om transparens i ressursbruk er APIM-basert kostnadssporing spesielt verdifull. Mange statlige virksomheter deler AI-infrastruktur på tvers av avdelinger og prosjekter, og trenger mekanismer for å fordele kostnader rettferdig. Denne referansen dekker token-telling fra responser, modell-routing-tracking, chargeback-tagging, integrasjon med Azure Cost Management, og egendefinerte metriker.
|
|
|
|
APIM tilbyr innebygde policyer for å emittere token-metriker (`llm-emit-token-metric`) og logge LLM API-requests med fullstendig token-bruk. Kombinert med Azure Monitor, Application Insights og Cost Management gir dette en komplett pipeline for AI-kostnadssporing fra request til faktura.
|
|
|
|
---
|
|
|
|
## Token Counting from Responses
|
|
|
|
### Azure OpenAI Token Usage
|
|
|
|
Hver Azure OpenAI-respons inkluderer token-bruk i `usage`-feltet:
|
|
|
|
```json
|
|
{
|
|
"id": "chatcmpl-abc123",
|
|
"object": "chat.completion",
|
|
"usage": {
|
|
"prompt_tokens": 150,
|
|
"completion_tokens": 250,
|
|
"total_tokens": 400
|
|
}
|
|
}
|
|
```
|
|
|
|
### llm-emit-token-metric Policy
|
|
|
|
Den primære policyen for å emittere token-metriker til Azure Monitor:
|
|
|
|
```xml
|
|
<outbound>
|
|
<base />
|
|
<llm-emit-token-metric namespace="ai-cost-metrics">
|
|
<!-- Maks 5 custom dimensions per policy (Azure Monitor-grense) -->
|
|
<dimension name="Subscription" value="@(context.Subscription.Name)" />
|
|
<dimension name="Product" value="@(context.Product.Name)" />
|
|
<dimension name="UserId"
|
|
value="@(context.Request.Headers.GetValueOrDefault("X-User-Id", "unknown"))" />
|
|
<dimension name="Department"
|
|
value="@(context.Request.Headers.GetValueOrDefault("X-Department", "unassigned"))" />
|
|
<dimension name="CostCenter"
|
|
value="@(context.Request.Headers.GetValueOrDefault("X-Cost-Center", "default"))" />
|
|
</llm-emit-token-metric>
|
|
</outbound>
|
|
```
|
|
|
|
**Viktig (MCP 2026-06):** `llm-emit-token-metric` tillater **maks 5 custom dimensions per policy** (Azure Monitor-grense — 5 default-dimensjoner brukes allerede av tjenesten; eksempelet over er derfor trimmet fra 8 til 5). Flere dimensjoner gir policy-feil. Policyen konfigureres i **inbound**-seksjonen og virker for OpenAI Chat Completions/Responses, **Anthropic Messages API** (API Management v2-tiers) og **Google Vertex AI**. Token-kategoriene inkluderer nå (preview) også cached-, reasoning- og thinking-tokens i tillegg til prompt/completion/total. Sett `include_usage=true` ved streaming, ellers blir token-tellingen unøyaktig.
|
|
|
|
### Token-typer og Kostnader
|
|
|
|
| Token-type | Beskrivelse | Kostnadsandel |
|
|
|-----------|-------------|---------------|
|
|
| Prompt tokens | Input-tokens (brukerens melding + system prompt) | Typisk 30-50% av kostnad |
|
|
| Completion tokens | Output-tokens (modellens svar) | Typisk 50-70% av kostnad |
|
|
| Cached tokens | Tokens fra prompt caching | Rabattert (opptil 50%) |
|
|
| Total tokens | Sum av prompt + completion | Grunnlag for fakturering |
|
|
|
|
### Kostnadsberegning per Request
|
|
|
|
```xml
|
|
<outbound>
|
|
<base />
|
|
<!-- Beregn estimert kostnad per request -->
|
|
<set-variable name="estimated-cost-nok" value="@{
|
|
// Eksempel: GPT-4o priser (tilpasses faktiske priser)
|
|
var promptRate = 0.025m; // kr per 1000 prompt tokens
|
|
var completionRate = 0.10m; // kr per 1000 completion tokens
|
|
|
|
var body = context.Response.Body.As<JObject>(preserveContent: true);
|
|
var usage = body?["usage"];
|
|
if (usage == null) return "0";
|
|
|
|
var promptTokens = usage["prompt_tokens"]?.Value<decimal>() ?? 0;
|
|
var completionTokens = usage["completion_tokens"]?.Value<decimal>() ?? 0;
|
|
|
|
var cost = (promptTokens / 1000m * promptRate) +
|
|
(completionTokens / 1000m * completionRate);
|
|
|
|
return cost.ToString("F4");
|
|
}" />
|
|
|
|
<!-- Legg til kostnadsinfo i response header -->
|
|
<set-header name="X-Estimated-Cost-NOK" exists-action="override">
|
|
<value>@((string)context.Variables["estimated-cost-nok"])</value>
|
|
</set-header>
|
|
</outbound>
|
|
```
|
|
|
|
---
|
|
|
|
## Model Routing Tracking
|
|
|
|
### Spore Hvilken Modell som Brukes
|
|
|
|
Når backend pools med forskjellige modeller brukes, er det viktig å spore hvilken modell og deployment som faktisk betjener requesten:
|
|
|
|
```xml
|
|
<outbound>
|
|
<base />
|
|
<!-- Ekstraher modell-info fra respons -->
|
|
<set-variable name="model-used" value="@{
|
|
var body = context.Response.Body.As<JObject>(preserveContent: true);
|
|
return body?["model"]?.ToString() ?? "unknown";
|
|
}" />
|
|
|
|
<set-variable name="deployment-id" value="@{
|
|
return context.Request.MatchedParameters.GetValueOrDefault("deployment-id", "unknown");
|
|
}" />
|
|
|
|
<!-- Emit metrikker med modell-dimensjon -->
|
|
<llm-emit-token-metric namespace="ai-model-metrics">
|
|
<dimension name="Model" value="@((string)context.Variables["model-used"])" />
|
|
<dimension name="DeploymentId" value="@((string)context.Variables["deployment-id"])" />
|
|
<dimension name="Backend" value="@(context.Request.Url.Host)" />
|
|
<dimension name="DeploymentType"
|
|
value="@(context.Request.Url.Host.Contains("ptu") ? "PTU" : "PayGo")" />
|
|
</llm-emit-token-metric>
|
|
</outbound>
|
|
```
|
|
|
|
### Modell-pris-mapping
|
|
|
|
| Modell | Prompt (kr/1K tokens) | Completion (kr/1K tokens) | Type |
|
|
|--------|----------------------|--------------------------|------|
|
|
| GPT-4o | 0.025 | 0.10 | Standard |
|
|
| GPT-4o-mini | 0.0015 | 0.006 | Standard |
|
|
| GPT-4 Turbo | 0.10 | 0.30 | Standard |
|
|
| text-embedding-ada-002 | 0.001 | N/A | Embedding |
|
|
| text-embedding-3-large | 0.0013 | N/A | Embedding |
|
|
| PTU (alle modeller) | Fast pris/time | Fast pris/time | Provisioned |
|
|
|
|
**Merk:** Priser varierer og bør oppdateres jevnlig. PTU faktureres per time uavhengig av faktisk bruk.
|
|
|
|
---
|
|
|
|
## Chargeback Tagging
|
|
|
|
### Implementere Chargeback-modell
|
|
|
|
En effektiv chargeback-modell krever at hver AI-request tagges med identifiserbar informasjon:
|
|
|
|
```xml
|
|
<inbound>
|
|
<base />
|
|
|
|
<!-- Ekstraher chargeback-info fra JWT token -->
|
|
<set-variable name="department" value="@{
|
|
var jwt = context.Request.Headers.GetValueOrDefault("Authorization", "")
|
|
.Replace("Bearer ", "");
|
|
if (string.IsNullOrEmpty(jwt)) return "unknown";
|
|
var token = jwt.AsJwt();
|
|
return token?.Claims.GetValueOrDefault("department", "unassigned");
|
|
}" />
|
|
|
|
<set-variable name="cost-center" value="@{
|
|
var jwt = context.Request.Headers.GetValueOrDefault("Authorization", "")
|
|
.Replace("Bearer ", "");
|
|
if (string.IsNullOrEmpty(jwt)) return "default";
|
|
var token = jwt.AsJwt();
|
|
return token?.Claims.GetValueOrDefault("cost_center", "default");
|
|
}" />
|
|
|
|
<set-variable name="project-code" value="@{
|
|
return context.Request.Headers.GetValueOrDefault("X-Project-Code", "general");
|
|
}" />
|
|
</inbound>
|
|
|
|
<outbound>
|
|
<base />
|
|
|
|
<!-- Emit chargeback-metriker -->
|
|
<llm-emit-token-metric namespace="chargeback-metrics">
|
|
<dimension name="Department" value="@((string)context.Variables["department"])" />
|
|
<dimension name="CostCenter" value="@((string)context.Variables["cost-center"])" />
|
|
<dimension name="ProjectCode" value="@((string)context.Variables["project-code"])" />
|
|
<dimension name="Subscription" value="@(context.Subscription.Name)" />
|
|
<dimension name="Model" value="@{
|
|
var body = context.Response.Body.As<JObject>(preserveContent: true);
|
|
return body?["model"]?.ToString() ?? "unknown";
|
|
}" />
|
|
</llm-emit-token-metric>
|
|
|
|
<!-- Legg til chargeback-headers i respons -->
|
|
<set-header name="X-Chargeback-Department" exists-action="override">
|
|
<value>@((string)context.Variables["department"])</value>
|
|
</set-header>
|
|
<set-header name="X-Chargeback-Tokens" exists-action="override">
|
|
<value>@{
|
|
var body = context.Response.Body.As<JObject>(preserveContent: true);
|
|
return body?["usage"]?["total_tokens"]?.ToString() ?? "0";
|
|
}</value>
|
|
</set-header>
|
|
</outbound>
|
|
```
|
|
|
|
### APIM Products for Chargeback
|
|
|
|
Bruk APIM Products for å gruppere API-tilgang per avdeling:
|
|
|
|
| Product | Beskrivelse | Rate Limit | Chargeback |
|
|
|---------|-------------|-----------|-----------|
|
|
| AI-Standard | Standard AI-tilgang | 10K TPM | Avdelingsbudsjett |
|
|
| AI-Premium | Utvidet AI-tilgang | 50K TPM | Prosjektbudsjett |
|
|
| AI-Unlimited | Full tilgang (admin) | Ubegrenset | Sentralt budsjett |
|
|
|
|
```xml
|
|
<!-- Product-basert rate limiting -->
|
|
<inbound>
|
|
<base />
|
|
<choose>
|
|
<when condition="@(context.Product.Name == "AI-Standard")">
|
|
<llm-token-limit counter-key="@(context.Subscription.Id)"
|
|
tokens-per-minute="10000" />
|
|
</when>
|
|
<when condition="@(context.Product.Name == "AI-Premium")">
|
|
<llm-token-limit counter-key="@(context.Subscription.Id)"
|
|
tokens-per-minute="50000" />
|
|
</when>
|
|
</choose>
|
|
</inbound>
|
|
```
|
|
|
|
---
|
|
|
|
## Azure Cost Management Integration
|
|
|
|
### Log Analytics for Kostnadsdata
|
|
|
|
Token-bruk logges til Azure Monitor via LLM API-logging:
|
|
|
|
```
|
|
APIM → Diagnostic Settings → "Logs related to generative AI gateway"
|
|
→ Log Analytics Workspace → ApiManagementGatewayLlmLog
|
|
```
|
|
|
|
### KQL Query: Daglig Kostnad per Avdeling
|
|
|
|
```kusto
|
|
ApiManagementGatewayLlmLog
|
|
| where TimeGenerated > ago(30d)
|
|
| extend Department = tostring(CustomDimensions["Department"])
|
|
| extend Model = tostring(ModelDeployment)
|
|
| extend PromptTokens = toint(PromptTokens)
|
|
| extend CompletionTokens = toint(CompletionTokens)
|
|
// Pris-mapping (oppdater etter faktiske priser)
|
|
| extend PromptCostNOK = case(
|
|
Model contains "gpt-4o-mini", PromptTokens * 0.0000015,
|
|
Model contains "gpt-4o", PromptTokens * 0.000025,
|
|
Model contains "gpt-4", PromptTokens * 0.0001,
|
|
PromptTokens * 0.00001 // Default
|
|
)
|
|
| extend CompletionCostNOK = case(
|
|
Model contains "gpt-4o-mini", CompletionTokens * 0.000006,
|
|
Model contains "gpt-4o", CompletionTokens * 0.0001,
|
|
Model contains "gpt-4", CompletionTokens * 0.0003,
|
|
CompletionTokens * 0.00003 // Default
|
|
)
|
|
| extend TotalCostNOK = PromptCostNOK + CompletionCostNOK
|
|
| summarize
|
|
DailyCostNOK = sum(TotalCostNOK),
|
|
TotalTokens = sum(toint(TotalTokens)),
|
|
RequestCount = count()
|
|
by Department, bin(TimeGenerated, 1d)
|
|
| order by TimeGenerated desc, DailyCostNOK desc
|
|
```
|
|
|
|
### KQL Query: Månedlig Chargeback-rapport
|
|
|
|
```kusto
|
|
ApiManagementGatewayLlmLog
|
|
| where TimeGenerated > startofmonth(ago(0d))
|
|
| extend CostCenter = tostring(CustomDimensions["CostCenter"])
|
|
| extend Department = tostring(CustomDimensions["Department"])
|
|
| extend Model = tostring(ModelDeployment)
|
|
| summarize
|
|
TotalPromptTokens = sum(toint(PromptTokens)),
|
|
TotalCompletionTokens = sum(toint(CompletionTokens)),
|
|
TotalTokens = sum(toint(TotalTokens)),
|
|
RequestCount = count(),
|
|
UniqueUsers = dcount(tostring(CustomDimensions["UserId"]))
|
|
by CostCenter, Department, Model
|
|
| extend EstimatedCostNOK =
|
|
TotalPromptTokens * 0.000025 + TotalCompletionTokens * 0.0001
|
|
| order by EstimatedCostNOK desc
|
|
```
|
|
|
|
### Azure Workbook for Kostnadsdashboard
|
|
|
|
APIM tilbyr et innebygd Analytics-dashboard for LLM-APIer:
|
|
|
|
```
|
|
1. APIM → Monitoring → Analytics → Language models
|
|
2. Viser: Token consumption, Request count, Modell-fordeling
|
|
3. Filtrer etter tidsperiode og API
|
|
```
|
|
|
|
For egendefinert dashboard:
|
|
|
|
```
|
|
1. Azure Monitor → Workbooks → New
|
|
2. Legg til KQL-queries for chargeback
|
|
3. Visualiser med tabeller, grafer, kart
|
|
4. Del med stakeholders via Azure RBAC
|
|
```
|
|
|
|
---
|
|
|
|
## Custom Metrics
|
|
|
|
### Emit Custom Metriker med Policy Expressions
|
|
|
|
```xml
|
|
<outbound>
|
|
<base />
|
|
|
|
<!-- Emit custom metrikk for estimert kostnad -->
|
|
<emit-metric name="ai-estimated-cost"
|
|
value="@{
|
|
var body = context.Response.Body.As<JObject>(preserveContent: true);
|
|
var usage = body?["usage"];
|
|
if (usage == null) return 0d;
|
|
|
|
var prompt = usage["prompt_tokens"]?.Value<double>() ?? 0;
|
|
var completion = usage["completion_tokens"]?.Value<double>() ?? 0;
|
|
|
|
return (prompt * 0.000025) + (completion * 0.0001);
|
|
}"
|
|
namespace="ai-cost">
|
|
<dimension name="Department"
|
|
value="@((string)context.Variables.GetValueOrDefault("department", "unknown"))" />
|
|
<dimension name="CostCenter"
|
|
value="@((string)context.Variables.GetValueOrDefault("cost-center", "default"))" />
|
|
</emit-metric>
|
|
|
|
<!-- Standard token-metriker -->
|
|
<llm-emit-token-metric namespace="ai-tokens">
|
|
<dimension name="Department"
|
|
value="@((string)context.Variables.GetValueOrDefault("department", "unknown"))" />
|
|
<dimension name="Model" value="@{
|
|
var body = context.Response.Body.As<JObject>(preserveContent: true);
|
|
return body?["model"]?.ToString() ?? "unknown";
|
|
}" />
|
|
</llm-emit-token-metric>
|
|
</outbound>
|
|
```
|
|
|
|
### Azure Monitor Alerts for Kostnadsoverskridelse
|
|
|
|
```bicep
|
|
resource costAlert 'Microsoft.Insights/metricAlerts@2018-03-01' = {
|
|
name: 'ai-cost-threshold-alert'
|
|
location: 'global'
|
|
properties: {
|
|
severity: 2
|
|
evaluationFrequency: 'PT1H'
|
|
windowSize: 'PT24H'
|
|
criteria: {
|
|
'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria'
|
|
allOf: [
|
|
{
|
|
name: 'DailyTokenBudgetExceeded'
|
|
metricNamespace: 'ai-tokens'
|
|
metricName: 'Total Tokens'
|
|
operator: 'GreaterThan'
|
|
threshold: 1000000 // 1M tokens per dag
|
|
timeAggregation: 'Total'
|
|
}
|
|
]
|
|
}
|
|
actions: [
|
|
{ actionGroupId: actionGroup.id }
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
### Eksport til Power BI for Rapportering
|
|
|
|
```kusto
|
|
// Eksporter data til Power BI via Log Analytics
|
|
ApiManagementGatewayLlmLog
|
|
| where TimeGenerated > ago(90d)
|
|
| project
|
|
Timestamp = TimeGenerated,
|
|
Department = tostring(CustomDimensions["Department"]),
|
|
CostCenter = tostring(CustomDimensions["CostCenter"]),
|
|
Model = ModelDeployment,
|
|
PromptTokens = toint(PromptTokens),
|
|
CompletionTokens = toint(CompletionTokens),
|
|
TotalTokens = toint(TotalTokens),
|
|
SubscriptionName = tostring(CustomDimensions["Subscription"])
|
|
```
|
|
|
|
---
|
|
|
|
## FinOps Integrasjon
|
|
|
|
### Azure Cost Management Tags
|
|
|
|
Kombiner APIM-metriker med Azure resource tags for helhetlig kostnadsbilde:
|
|
|
|
| Tag | Formål | Eksempel |
|
|
|-----|--------|---------|
|
|
| `Department` | Avdelingstilhørighet | "IT-seksjonen" |
|
|
| `CostCenter` | Kostnadssenter-kode | "KS-4210" |
|
|
| `Environment` | Miljø | "production" |
|
|
| `Project` | Prosjektkode | "AI-chatbot-2026" |
|
|
|
|
### Kostnadsmodeller for AI
|
|
|
|
| Modell | Fordeler | Ulemper |
|
|
|--------|---------|--------|
|
|
| Per-token chargeback | Presis, rettferdig | Kompleks å administrere |
|
|
| Flat rate per avdeling | Enkelt, forutsigbart | Urettferdig for lavbrukere |
|
|
| Tier-basert (freemium) | Balansert, insentiverer effektivitet | Krever grensehåndtering |
|
|
| PTU-allokering | Fast kostnad, forutsigbart | Ingen fleksibilitet |
|
|
|
|
---
|
|
|
|
## Referanser
|
|
|
|
- [AI gateway in Azure API Management - Observability and governance](https://learn.microsoft.com/en-us/azure/api-management/genai-gateway-capabilities#observability-and-governance) — Oversikt over token-metriker
|
|
- [llm-emit-token-metric policy](https://learn.microsoft.com/en-us/azure/api-management/llm-emit-token-metric-policy) — Policy-referanse for token-metriker
|
|
- [Log token usage, prompts, and completions](https://learn.microsoft.com/en-us/azure/api-management/api-management-howto-llm-logs) — LLM API-logging
|
|
- [Plan and manage costs for API Management](https://learn.microsoft.com/en-us/azure/api-management/plan-manage-costs) — APIM-kostnader i Cost Management
|
|
- [Azure Cost Management overview](https://learn.microsoft.com/en-us/azure/cost-management-billing/costs/overview-cost-management) — Helhetlig kostnadsadministrasjon
|
|
|
|
---
|
|
|
|
## For Cosmo
|
|
|
|
- **Bruk denne referansen** når kunder trenger å implementere kostnadssporing og intern fakturering for AI-tjenester gjennom APIM.
|
|
- Start med `llm-emit-token-metric` policy med Department- og CostCenter-dimensjoner — dette gir umiddelbar synlighet i token-bruk per avdeling.
|
|
- For norsk offentlig sektor: Anbefal tier-basert chargeback-modell med APIM Products som mapper til avdelingsbudsjetter. Flat rate er for enkelt, per-token er for komplekst for de fleste.
|
|
- Husk at PTU-kostnader er faste per time — chargeback for PTU bør baseres på allokert kapasitet, ikke faktisk bruk.
|
|
- Kombiner APIM-metriker med Azure resource tags for å gi et helhetlig bilde i Cost Management. APIM-metriker alene viser kun token-bruk, ikke infrastrukturkostnader.
|