# Cost and Budget — tarifa, cálculo e teto de gasto de um agente

> **Scope:** stacks=["dotnet"]
> **Layer:** 1 (on-keyword)
> **Keywords:** custo de llm, cost middleware, UsageCostMiddleware, teto de custo, budget middleware, tarifa por token, cost_usd, orçamento do agente
> **Read by Claude in:** implement (ao registrar custo ou impor teto) e troubleshoot (fatura maior que o esperado, custo nulo no relatório)

**Verified against:** Microsoft.Extensions.AI 10.9.0 + Microsoft.Agents.AI 1.20.0 (ai-pin 2026-09-08); provado por `templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostMiddleware.cs`, `.../Middleware/UsageCostRecord.cs` e `.../Providers/ModelAlias.cs` — o middleware, o record de custo não-nulo e a conta de tarifa são o que o kit compila. A ausência de qualquer propriedade de preço em `UsageDetails` foi **medida por reflexão sobre `Microsoft.Extensions.AI.Abstractions` 10.9.0** em 2026-09-08. O teto de orçamento é **verificação documental** — o ai-kit não traz um middleware de orçamento. Last-verified: 2026-09-08.

---

## Tarifa no registry

**O SDK dá tokens e não dá dinheiro.** Medido por reflexão sobre
`Microsoft.Extensions.AI.Abstractions` 10.9.0 em 2026-09-08, `UsageDetails` expõe:

`InputTokenCount` · `OutputTokenCount` · `TotalTokenCount` · `CachedInputTokenCount` ·
`ReasoningTokenCount` · os pares de texto e áudio (`InputTextTokenCount`, `InputAudioTokenCount`,
`OutputTextTokenCount`, `OutputAudioTokenCount`) · `AdditionalCounts` · e o método
`Add(UsageDetails)`.

**Nenhuma propriedade de preço.** Nem tarifa, nem moeda, nem custo. Isso não é lacuna do pacote: é a
divisão certa. A tarifa muda por contrato, por região e por semana; um SDK que a embutisse estaria
errado no dia seguinte.

**Logo a tarifa é do projeto, e mora por alias no registry** — o mesmo lugar onde já moram provider,
modelo e protocolo. A forma que o kit compila:

| Campo | Significado |
|---|---|
| `InputPerMTok` | USD por 1M de tokens de entrada |
| `OutputPerMTok` | USD por 1M de tokens de saída |
| `CachedInputPerMTok` | USD por 1M de tokens de entrada servidos do cache. **Nulo = não há desconto declarado**, e então token cacheado é cobrado como entrada normal. Nulo **nunca** significa "de graça" |

**E a tarifa precisa de data — que NÃO cabe no `pricing`.** Tarifa envelhece, e um preço sem data é
um número que ninguém sabe se ainda vale. Mas o exemplar estampado acima **recusa** um campo de data:
medido em 2026-09-08, `ModelRegistry` valida o objeto `pricing` contra uma allowlist fechada de
exatamente três chaves (`inputPerMTok`, `outputPerMTok`, `cachedInputPerMTok`) e chama
`RejectUnknownProperties` — qualquer quarta chave **derruba a carga do registry**. A allowlist do
alias também não tem chave de data.

Onde a data mora, então:

| Opção | Custo |
|---|---|
| **Comentário JSONC no registry**, ao lado do bloco `pricing` | Zero: o parser do exemplar já roda com `JsonCommentHandling.Skip`, então o comentário é lido e descartado sem erro. É a opção default |
| Campo novo na allowlist (`verifiedAt`) | Muda `.cs` — decisão de projeto, não deste standard |
| Arquivo de procedência separado, versionado ao lado do registry | Serve quando a tarifa vem de contrato e precisa de histórico |

**Nunca** escreva a data como uma quarta chave dentro de `pricing` sem antes abrir a allowlist: o
registry para de carregar, e o sintoma (falha na inicialização) não aponta para a tarifa.

**Alias sem tarifa precisa dizer por quê.** O registry do kit **recusa a carga** de um alias que não
declare nem tarifa nem uma dispensa escrita (`pricingWaiver`) — modelo local, por exemplo, onde a
conta é a máquina e não o token. O esquecimento deixou de ser um caminho possível: ou há tarifa, ou
há motivo.

---

## O middleware de custo

**Um middleware, não uma fórmula copiada.**

A dor, literal e contada: num projeto do acervo a **mesma expressão de custo** aparece em **cinco
call-sites, distribuídos em quatro arquivos** (um dos arquivos hospeda dois agentes). E o dado já
era único — o diagnóstico do próprio projeto registra: *"o SDK expõe as contagens de token, mas não
preços; a duplicação é do cálculo, não do dado"*.

Duas cópias da mesma conta divergem. A divergência aparece na fatura, que é o pior lugar para
descobri-la.

A forma: um `DelegatingChatClient` que lê o `Usage` da resposta, chama **a** conta (a que já existe,
no tipo de tarifa) e publica **uma linha por chamada** num sink de custo. O middleware não contém
aritmética de tarifa — ele converte, chama e carimba.

### Onde ele entra no pipeline, e por que a ordem não é estética

O encaixe de custo fica **acima** da invocação de funções, com a telemetria de chat **abaixo** dela:

```
usage-cost  ->  function-invocation  ->  OpenTelemetry  ->  cliente do provider
```

O motivo é medido, não argumentado. **Acima** do cliente que invoca funções, o middleware vê **uma**
resposta cujo `Usage` já traz o total de todas as voltas de tool, e publica **uma** linha por
chamada. **Abaixo** dele, veria as respostas parciais — e leria o mesmo objeto de uso que o próprio
cliente de invocação **muta** para acumular, o que faria a mesma conta dar resultado diferente
conforme a hora de ler.

---

## Armadilhas de fórmula

Três formas de errar a conta com todos os números certos:

**(1) `ReasoningTokenCount` é cobrado como saída.** Uma fórmula que soma apenas entrada + saída sem
conferir **como aquele provider bilha o raciocínio** subestima o custo de todo modelo de
raciocínio — exatamente os mais caros.

**(2) `CachedInputTokenCount` tem tarifa própria, e ignorá-lo superestima.** O detalhe que derruba
implementações: **o cacheado é subconjunto da entrada**, não uma parcela a mais. Somar entrada +
cacheado conta o mesmo token duas vezes. A conta correta separa a parte cacheada da parte cheia:

```
custo = (entrada - cacheado) * tarifaEntrada
      + cacheado * (tarifaCache ?? tarifaEntrada)
      + saida * tarifaSaida
```

E quando não há tarifa de cache declarada, **toda** a entrada é cobrada pela tarifa de entrada —
nunca de graça.

**(3) Taxa fixa por operação hospedada não aparece em `UsageDetails`.** Busca na web, interpretador
de código e afins são cobrados por **operação**, não por token. Precedente de campo: um campo
dedicado de custo de busca no registro de alias, com vinte linhas de comentário explicando que
ignorá-lo subestimaria o teto. Se o agente usa uma dessas, a conta por token está estruturalmente
incompleta, e o standard exige que isso seja declarado em vez de descoberto.

---

## cost_usd nunca nulo

**`cost_usd` nunca é nulo.** Alias sem tarifa registra tokens e custo **zero com a ausência
sinalizada** — nunca `null`, nunca um número inventado.

Um custo nulo atravessa três camadas e vira um buraco no relatório, e ninguém consegue distinguir
"não custou" de "não medimos". A distinção precisa viajar **junto** com o número, num estado
explícito. Os três estados que o kit compila:

| Estado | Significado | Pode somar numa fatura? |
|---|---|---|
| `Computed` | Tarifa do alias aplicada sobre uso reportado. O único em que o valor é **medição** | Sim, sem ressalva |
| `Waived` | Alias sem tarifa **com dispensa escrita**. Zero por decisão registrada, e o motivo viaja junto | Sim, ciente de que é zero por decisão |
| `UsageMissing` | O provider não reportou uso. Os tokens são desconhecidos, logo o custo também: o zero é **ausência de medição**, não gratuidade | Só com a ressalva explícita |

**Precedente bom, do acervo:** um projeto trata uso nulo como zero com o comentário *"0 é o fallback
honesto (custo subestimado, nunca inventado)"* — e persiste custo em **todos** os caminhos, inclusive
nos de falha: um agente que devolve "não encontrado" devolve também *"o custo já pago visível"*.

**Precedente ruim, e é regressão viva:** outro projeto grava `cost_usd = null` **hoje, em
produção**, porque a tabela de preços sai vazia da configuração. O código está correto; a
configuração está vazia; e o resultado é que o produto não sabe quanto gasta.

---

## Teto de orçamento

**A unidade é parâmetro.** Por lead, por tenant, por operação, por dia — o standard não inventa uma
"unidade de negócio" que nenhum projeto do acervo tem. O que ele fixa é a **forma** do teto.

O acervo tem exatamente **um** teto duro (um limite de custo por lead) e ele revela uma dor de
**lugar**, não de unidade: o teto mora **dentro de uma tool**, e por isso não vale para o pipeline
inteiro. Tudo o que a tool não executa passa por fora dele. Os demais "tetos" do acervo são
instrução textual ao modelo, chamados **no próprio código** de *"freio soft"* — o que é uma
descrição honesta: o modelo não é um mecanismo de controle de gasto.

**As quatro regras:**

1. **O teto mora no pipeline, não dentro de uma tool.** Um teto que só uma tool respeita mede uma
   fração do gasto.
2. **Alerta antes de bloquear.** Um limiar de aviso, com a unidade e o valor acumulado, dá ao
   operador a chance de agir; um bloqueio sem aviso prévio aparece como incidente.
3. **O bloqueio devolve ao modelo uma frase acionável** — *"orçamento esgotado, conclua com o que
   você já tem"* — e **nunca uma exceção**. Uma exceção no meio de um turno perde o trabalho já
   pago; uma frase permite fechar a conversa com o que existe.
4. **A unidade e o valor são configuração**, e o valor efetivo aparece no registro do turno, para
   que "por que bloqueou" tenha resposta.

> **Lacuna declarada.** O par compilável desta seção seria um `BudgetMiddleware` em
> `templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/`. **Ele não existe** — nem no ai-kit atual
> nem no plano que o construiu. Por isso o cabeçalho declara esta parte como documental.

---

## Medir o total, não o piso

**Um middleware no pipeline de chat mede o que passa pelo pipeline. O que não passa por ele precisa
ser declarado.**

O caso medido, e ele é o alerta inteiro: um projeto declara o contrato de *"no máximo 2 chamadas de
LLM por turno"* — e a própria documentação do projeto reconhece que essa medição é **piso, não
total**. Não são contadas: transcrição de áudio, visão (uma chamada por imagem e **uma por página**
de PDF), embeddings de memória, embeddings de base de conhecimento, análise de memória e
sumarização. Um turno com um PDF de três páginas chega a **cerca de nove** chamadas ao provider.

A regra: faça o inventário das chamadas ao provider que **não** passam pelo pipeline instrumentado,
e escreva-o. Um número que mede metade do gasto e se apresenta como total é pior que nenhum número,
porque produz confiança.

---

## Fronteira com observability-patterns

`ai-agents-observability-patterns` é o **dono canônico** de OpenTelemetry: setup, atributos de span,
os nomes `gen_ai.*`, enrichment e a tabela de monitoramento. Ele continua sendo o lugar de "como
instrumentar".

**Este** standard é dono de: a **tarifa** (onde mora, com que forma, com que data), o **cálculo**
(a conta, e as três armadilhas), o **estado** do custo (`Computed`/`Waived`/`UsageMissing`) e o
**teto** de gasto.

A delegação é **de mão dupla** e está escrita nos dois arquivos: quem chega pelo lado da
observabilidade procurando tarifa é mandado para cá; quem chega aqui procurando atributo de span é
mandado para lá. Nenhum dos dois duplica o outro.

---

## Anti-patterns

| Anti-pattern | Por quê é problema | Solução |
|---|---|---|
| Fórmula de custo copiada em cada agente | Duas cópias divergem, e a divergência aparece na fatura | Uma conta, num tipo; um middleware que a chama |
| Tarifa embutida no código | Muda por contrato e por semana; vira literal errado | Tarifa por alias no registry |
| Tarifa sem data de verificação | Ninguém sabe se o preço ainda vale | Data ao lado do valor — em comentário JSONC, não como quarta chave do `pricing` |
| Alias sem tarifa e sem motivo escrito | O custo some em silêncio | Ou tarifa, ou dispensa escrita — o registry recusa o resto |
| `cost_usd` nulo | Buraco no relatório; "não custou" e "não medimos" viram a mesma coisa | Zero com estado explícito |
| Zero anônimo (sem estado) | Some a diferença entre dispensa e ausência de medição | `Computed` / `Waived` / `UsageMissing` |
| Somar entrada + cacheado | Conta o mesmo token duas vezes: o cacheado é subconjunto | Separe a parte cheia da cacheada |
| Ignorar `ReasoningTokenCount` | Subestima justamente os modelos mais caros | Confira como o provider bilha o raciocínio |
| Ignorar taxa fixa de operação hospedada | A conta por token é estruturalmente incompleta | Declare a taxa, ou declare a incompletude |
| Teto dentro de uma tool | Mede uma fração do gasto do pipeline | Teto no pipeline |
| Teto como instrução textual ao modelo | O modelo não é mecanismo de controle de gasto | Código, com alerta antes do bloqueio |
| Bloqueio por exceção | Perde o trabalho já pago no meio do turno | Frase acionável de volta ao modelo |
| Contar só o que passa pelo pipeline e chamar de total | Produz confiança num número que mede metade | Inventário escrito do que não passa |

---

## Checklist (verifiable by morph-eval)

- [ ] A conta de custo existe em **um** lugar; nenhum agente a repete
- [ ] A tarifa mora por alias no registry, e a **data** dela está registrada FORA do objeto `pricing` (comentário JSONC, campo próprio na allowlist, ou arquivo de procedência) — a allowlist do exemplar tem exatamente 3 chaves e recusa uma quarta
- [ ] Nenhum alias sem tarifa e sem motivo escrito
- [ ] O middleware de custo está **acima** da invocação de funções no pipeline
- [ ] `cost_usd` nunca é nulo, e cada linha carrega o estado (`Computed`/`Waived`/`UsageMissing`)
- [ ] A fórmula trata o cacheado como subconjunto da entrada
- [ ] `ReasoningTokenCount` e taxas fixas de operação hospedada estão tratados ou declarados
- [ ] Custo é persistido também nos caminhos de falha
- [ ] Se há teto: ele está no pipeline, alerta antes de bloquear, e o bloqueio devolve frase, não exceção
- [ ] Existe inventário escrito das chamadas ao provider que **não** passam pelo pipeline instrumentado

---

## References

- `ai-agents-observability-patterns` — dono de OpenTelemetry, dos atributos `gen_ai.*` e da tabela
  de monitoramento; a delegação de tarifa e teto é recíproca
- `ai-agents-providers-model-registry` — Layer 0: o schema do documento onde a tarifa mora
- `ai-agents-llm-runtime-defaults` — timeout, retry e o pin de SDK; a segunda chamada paga cujo custo
  entra no critério de saída de `ai-agents-conversational-agent-with-phases`
- `ai-agents-middleware-patterns` — o **mecanismo** de middleware de chat; este standard usa, não repete
- `ai-agents-service-tier-flex` — reduzir tarifa em trabalho assíncrono
- `ai-agents-batching` — reduzir tarifa processando N prompts em lote

---

*MORPH-SPEC by Polymorphism Tech — ai-agents/cost-and-budget.md v1.0 (2026-09-08)*
