Personalize this lesson
Adapt explanations and teaching visuals to your background and preferred voice.
In our worked PolicyOps release receipt, an on-call alert fires at 09:12: one traffic day plus its nightly evaluation costs $23.92. A 30-day projection still fits under the $750 budget, but leaves only $32.44. The release can pass its hard gate and still deserve a closer look.
The dashboard labels 3,200 requests as cache hits. Some returned an already evaluated answer and made no model call. Others reused a provider prompt prefix and still generated new text. Collapse those events into one label and you can't tell avoided spend from discounted spend, or know which correctness check failed.
PolicyOps's support assistant is built around a large language model (LLM). It also handles live-order lookups and cited exception answers. We need a cost ledger for one evaluated release: provider-reported token usage, both cache kinds, output savings that preserve the answer contract, work that can wait, and a promotion decision. The next lesson will consume that budget contract when it routes and falls back. First classify each call.
The accounting boundary
Before pricing, classify what happened. "Cache hit" can mean no model call or a generated answer with cheaper input. Use one row for each outcome:
| Decision | What executes? | What belongs in this ledger? |
|---|---|---|
| Semantic answer hit from the previous lesson | Stored evaluated answer returned | No generation charge; record avoided generation separately |
| Generated answer with provider prompt-cache hit | Model still produces a new answer | Fresh input, cached input, and output token charges |
| Generated answer without prompt-cache hit | Model reads full input and produces a new answer | Fresh input and output token charges |
| Offline evaluation batch | Model runs later, outside interactive path | Batch-eligible token spend and completion contract |
Only the semantic answer hit has no generated usage. A provider prompt cache reuses matching prefix work, then generates a fresh answer. That distinction determines both the invoice row and the correctness test you need.
Ask two questions in order: did we return an evaluated answer, and if not, did the provider reuse an input prefix? The flow below turns those answers into billable categories.

Use a dated rate card, not remembered prices
Treat provider pricing as versioned configuration. This lab's teaching fixture freezes OpenAI's GPT-5.4 short-context row. Checked against the official pricing and model pages on August 21, 2026, it matches the May 31, 2026 snapshot used in the code: standard input $2.50 per million tokens, cached input $0.25 per million, and output $15.00 per million.[1][2]
OpenAI also publishes a long-context row. Above 272K input tokens, the full session uses $5.00 / $0.50 / $22.50 for input, cached input, and output. Every trace here stays below that cliff, so the ledger selects the short-context row before doing any token arithmetic.[1][2]
Batch rates are another published row, not a local "divide every Standard rate by two" rule. The displayed Batch cached-input rate is $0.13 per million tokens; halving $0.25 gives $0.125, so local arithmetic would lose the displayed rate. This dated fixture teaches ledger shape, not a permanent price promise. Refresh production cards from current provider documentation before forecasting a release.[3][1]
The usage trace, not a character estimate, settles the bill after a model call. Read total input tokens, the cached subset, and output tokens from that trace. Fresh input is total input minus cached input.
Reasoning models add a tempting accounting trap. For GPT-5.4, provider-reported output_tokens already includes hidden reasoning tokens billed at the output rate. output_tokens_details.reasoning_tokens explains part of that total; adding it again would double-charge. A call can also hit max_output_tokens while thinking, leaving no visible answer while its output tokens still bill.[4]
Before running the first cell, predict the split for a 1,800-token trace with 400 cached tokens: 1,400 fresh tokens and 400 cached tokens. The rate card and usage objects make that subtraction explicit.
1from collections import defaultdict
2from dataclasses import dataclass, replace
3from decimal import Decimal, ROUND_HALF_UP
4
5MILLION = Decimal("1000000")
6CENT = Decimal("0.01")
7
8def dollars(amount: Decimal) -> Decimal:
9 return amount.quantize(CENT, rounding=ROUND_HALF_UP)
10
11@dataclass(frozen=True)
12class TokenRates:
13 input_per_1m: Decimal
14 cached_input_per_1m: Decimal
15 output_per_1m: Decimal
16
17@dataclass(frozen=True)
18class RateCard:
19 rate_card_id: str
20 standard: TokenRates
21 batch: TokenRates
22
23@dataclass(frozen=True)
24class Usage:
25 input_tokens: int
26 cached_input_tokens: int
27 output_tokens: int
28 reasoning_tokens: int = 0
29
30 def __post_init__(self) -> None:
31 if not 0 <= self.cached_input_tokens <= self.input_tokens:
32 raise ValueError("cached input must be a subset of input tokens")
33 if min(self.input_tokens, self.output_tokens) < 0:
34 raise ValueError("token counts cannot be negative")
35 if not 0 <= self.reasoning_tokens <= self.output_tokens:
36 raise ValueError("reasoning tokens must be a subset of output tokens")
37
38rate_card = RateCard(
39 rate_card_id="openai-gpt-5.4-short-context-2026-05-31",
40 standard=TokenRates(
41 input_per_1m=Decimal("2.50"),
42 cached_input_per_1m=Decimal("0.25"),
43 output_per_1m=Decimal("15.00"),
44 ),
45 batch=TokenRates(
46 input_per_1m=Decimal("1.25"),
47 cached_input_per_1m=Decimal("0.13"),
48 output_per_1m=Decimal("7.50"),
49 ),
50)
51
52print(rate_card.rate_card_id)
53print(f"standard_input_per_1m=${rate_card.standard.input_per_1m}")
54print(f"standard_cached_input_per_1m=${rate_card.standard.cached_input_per_1m}")
55print(f"standard_output_per_1m=${rate_card.standard.output_per_1m}")
56print(f"batch_input_per_1m=${rate_card.batch.input_per_1m}")
57print(f"batch_cached_input_per_1m=${rate_card.batch.cached_input_per_1m}")
58print(f"batch_output_per_1m=${rate_card.batch.output_per_1m}")1openai-gpt-5.4-short-context-2026-05-31
2standard_input_per_1m=$2.50
3standard_cached_input_per_1m=$0.25
4standard_output_per_1m=$15.00
5batch_input_per_1m=$1.25
6batch_cached_input_per_1m=$0.13
7batch_output_per_1m=$7.50Price one generated answer
PolicyOps's public-policy-answer path uses a long evaluated instruction prefix and cites policy evidence in every generated response. Take the stable public question "How long are revoked access tokens retained?" A cold request reports 1,800 input tokens and 180 output tokens. Before looking at the formula, predict which side costs more: output has the higher rate, but input has ten times as many tokens.
Split each billable category instead of multiplying one blended token count by one price.
1@dataclass(frozen=True)
2class PricedUsage:
3 fresh_input_usd: Decimal
4 cached_input_usd: Decimal
5 output_usd: Decimal
6
7 @property
8 def total_usd(self) -> Decimal:
9 return self.fresh_input_usd + self.cached_input_usd + self.output_usd
10
11def price_usage(usage: Usage, card: RateCard = rate_card, *, batch: bool = False) -> PricedUsage:
12 rates = card.batch if batch else card.standard
13 fresh_input_tokens = usage.input_tokens - usage.cached_input_tokens
14 return PricedUsage(
15 fresh_input_usd=Decimal(fresh_input_tokens) * rates.input_per_1m / MILLION,
16 cached_input_usd=Decimal(usage.cached_input_tokens) * rates.cached_input_per_1m / MILLION,
17 output_usd=Decimal(usage.output_tokens) * rates.output_per_1m / MILLION,
18 )
19
20cold_policy_answer = Usage(input_tokens=1_800, cached_input_tokens=0, output_tokens=180)
21cold_cost = price_usage(cold_policy_answer)
22
23assert cold_cost.total_usd == Decimal("0.0072")
24assert rate_card.standard.output_per_1m > rate_card.standard.input_per_1m
25
26print(f"fresh_input=${cold_cost.fresh_input_usd:.6f}")
27print(f"output=${cold_cost.output_usd:.6f}")
28print(f"total=${cold_cost.total_usd:.6f}")
29print(f"output_share={(cold_cost.output_usd / cold_cost.total_usd):.1%}")1fresh_input=$0.004500
2output=$0.002700
3total=$0.007200
4output_share=37.5%The output token rate is higher, but this 1,800-input-token request still spends more dollars on input because it has ten times as many input tokens as output tokens. Both volume and per-category rate belong in the ledger.
That split also gives you two different latency levers. Prefix reuse can reduce repeated input processing and first-token delay, but it doesn't remove decode. A shorter compliant answer can reduce output spend and often decode time. Measure time to first token, output tokens, and end-to-end latency separately instead of treating one cost change as a latency result.[5][1][6]
Reasoning tokens ride inside that output charge. In OpenAI's published usage shape, output_tokens=1186 includes reasoning_tokens=1024, leaving 162 output tokens outside the reasoning detail in this fixture. Predict the price before running the cell: charge 1,186 output tokens once, not 1,186 plus 1,024. An incomplete call can spend all 1,024 output tokens on hidden reasoning and still bill decode without returning a visible answer.
1reasoning_answer = Usage(
2 input_tokens=1_800,
3 cached_input_tokens=0,
4 output_tokens=1_186,
5 reasoning_tokens=1_024,
6)
7incomplete_reasoning = Usage(
8 input_tokens=1_800,
9 cached_input_tokens=0,
10 output_tokens=1_024,
11 reasoning_tokens=1_024,
12)
13reasoning_cost = price_usage(reasoning_answer)
14incomplete_cost = price_usage(incomplete_reasoning)
15double_counted_output = Decimal("2210") * rate_card.standard.output_per_1m / MILLION
16
17assert reasoning_cost.output_usd == Decimal("1186") * rate_card.standard.output_per_1m / MILLION
18assert reasoning_cost.output_usd != double_counted_output
19assert incomplete_cost.output_usd == Decimal("1024") * rate_card.standard.output_per_1m / MILLION
20assert incomplete_cost.total_usd > Decimal("0")
21
22print(f"visible_plus_reasoning_output_tokens={reasoning_answer.output_tokens}")
23print(f"reasoning_detail={reasoning_answer.reasoning_tokens}")
24print(f"priced_output=${reasoning_cost.output_usd:.6f}")
25print(f"double_counted_output=${double_counted_output:.6f}")
26print(f"incomplete_hidden_output_tokens={incomplete_reasoning.output_tokens}")
27print(f"incomplete_total=${incomplete_cost.total_usd:.6f}")1visible_plus_reasoning_output_tokens=1186
2reasoning_detail=1024
3priced_output=$0.017790
4double_counted_output=$0.033150
5incomplete_hidden_output_tokens=1024
6incomplete_total=$0.019860The incomplete row still includes the $0.004500 fresh-input charge. Hidden thinking isn't free, and reasoning_tokens isn't a second billable bucket. Keep the detail field for diagnosis: it explains why a response was expensive or stopped before visible output.
Prefix reuse is still a generated answer
Prompt-cache eligibility is observed behavior, not a budget promise. OpenAI's guidance, checked on August 21, 2026, gives GPT-5.6 and later a 1,024-visible-token minimum and charges cache writes at 1.25× the uncached input rate. GPT-5.4 follows earlier-model behavior: 2,048 visible tokens is the documented minimum, some earlier models may occasionally cache shorter prefixes, and there is no additional cache-write fee. Copying the GPT-5.4 ledger onto GPT-5.6 without a write column would under-count.[2][6]
Caching also depends on an exact matching rendered prefix. Count a read only when returned usage reports cached_tokens > 0; put stable instructions and shared evidence first, then dynamic customer data. A prefix hit isn't a stored response, so the support model still answers the current question and still incurs output spend.[6]
The fixture below records an observed 1,280-token cache read. Treat that number as trace data, not as a guarantee that a prompt near any threshold will hit. Predict the arithmetic first: 1,800 total input tokens means 520 fresh tokens, while output remains 180 tokens. Any latency improvement here comes from input reuse, not from skipping decode.
1prefix_cached_policy_answer = Usage(
2 input_tokens=1_800,
3 cached_input_tokens=1_280,
4 output_tokens=180,
5)
6prefix_cost = price_usage(prefix_cached_policy_answer)
7savings = cold_cost.total_usd - prefix_cost.total_usd
8
9assert prefix_cost.total_usd == Decimal("0.004320")
10assert prefix_cost.output_usd == cold_cost.output_usd
11assert savings == Decimal("0.002880")
12
13print(f"cold_generation=${cold_cost.total_usd:.6f}")
14print(f"prefix_cached_generation=${prefix_cost.total_usd:.6f}")
15print(f"saved_per_generated_answer=${savings:.6f}")
16print(f"still_generated={prefix_cost.output_usd > 0}")1cold_generation=$0.007200
2prefix_cached_generation=$0.004320
3saved_per_generated_answer=$0.002880
4still_generated=TrueWhy does a provider prompt-cache hit still have an output charge?
Answer
It reuses processing for an exactly matching input prefix. The model still generates the new response, so output tokens remain billable.
Carry forward the semantic-cache decision
The preceding lesson approved semantic response reuse only for stable public-policy answers within one evaluated release scope. This ledger records that decision; it doesn't rebuild the vector index or threshold.
For "How long are revoked access tokens retained?", a stored-answer hit records zero generated tokens. A prompt-prefix hit records a new answer with cheaper input. Predict those rows before running the next cell: the first has $0 actual generation and a non-zero counterfactual, while the second has $0.004320 actual generation.
1@dataclass(frozen=True)
2class RequestOutcome:
3 feature: str
4 decision: str
5 usage: Usage | None
6 counterfactual_usage: Usage | None = None
7
8def generated_cost(outcome: RequestOutcome) -> Decimal:
9 if outcome.usage is None:
10 return Decimal("0")
11 return price_usage(outcome.usage).total_usd
12
13semantic_hit = RequestOutcome(
14 feature="public-policy-answer",
15 decision="SEMANTIC_ANSWER_HIT",
16 usage=None,
17 counterfactual_usage=prefix_cached_policy_answer,
18)
19prefix_hit = RequestOutcome(
20 feature="public-policy-answer",
21 decision="GENERATE_PREFIX_HIT",
22 usage=prefix_cached_policy_answer,
23)
24
25avoided_generation = price_usage(semantic_hit.counterfactual_usage).total_usd
26assert generated_cost(semantic_hit) == Decimal("0")
27assert generated_cost(prefix_hit) == Decimal("0.004320")
28
29print(f"semantic_hit_generation=${generated_cost(semantic_hit):.6f}")
30print(f"semantic_hit_avoided=${avoided_generation:.6f}")
31print(f"prefix_hit_generation=${generated_cost(prefix_hit):.6f}")1semantic_hit_generation=$0.000000
2semantic_hit_avoided=$0.004320
3prefix_hit_generation=$0.004320
Replay a day of traffic
A per-request price hides traffic shape. Replay one PolicyOps day before testing a shorter exception answer:
- 3,200 stable policy questions safely hit the semantic answer cache.
- 1,800 policy questions still generate, but reuse the evaluated prompt prefix.
- 3,000 live-order questions must generate from current tool evidence.
- 500 access exceptions generate a longer cited answer with the stable prefix.
Before running the replay, predict the ranking. Semantic hits are numerous but have no generated spend. Live-order calls always generate from current evidence, so their output and volume may dominate the billed total. Keep two answers separate: the counterfactual says what safe reuse avoided; actual spend says what belongs on this release ledger.
1@dataclass(frozen=True)
2class TrafficSlice:
3 outcome: RequestOutcome
4 requests: int
5
6daily_replay = [
7 TrafficSlice(semantic_hit, requests=3_200),
8 TrafficSlice(prefix_hit, requests=1_800),
9 TrafficSlice(
10 RequestOutcome(
11 feature="live-order-answer",
12 decision="GENERATE_LIVE_DATA",
13 usage=Usage(input_tokens=900, cached_input_tokens=0, output_tokens=95),
14 ),
15 requests=3_000,
16 ),
17 TrafficSlice(
18 RequestOutcome(
19 feature="access-exception-answer",
20 decision="GENERATE_PREFIX_HIT",
21 usage=Usage(input_tokens=2_200, cached_input_tokens=1_280, output_tokens=220),
22 ),
23 requests=500,
24 ),
25]
26
27actual_daily_spend = sum(
28 generated_cost(slice_.outcome) * slice_.requests
29 for slice_ in daily_replay
30)
31avoided_daily_generation = sum(
32 price_usage(slice_.outcome.counterfactual_usage).total_usd * slice_.requests
33 for slice_ in daily_replay
34 if slice_.outcome.counterfactual_usage is not None
35)
36
37assert actual_daily_spend == Decimal("21.761000")
38assert avoided_daily_generation == Decimal("13.824000")
39
40print(f"generated_daily_spend=${dollars(actual_daily_spend)}")
41print(f"semantic_hit_avoided_daily=${dollars(avoided_daily_generation)}")
42print(f"request_count={sum(slice_.requests for slice_ in daily_replay)}")1generated_daily_spend=$21.76
2semantic_hit_avoided_daily=$13.82
3request_count=8500The replay reports $21.76 of generated spend and $13.82 of avoided generation. Avoided generation is evidence, not an invoice credit, so keep it out of actual spend. If an answer hit later fails review, its quality failure matters before its apparent savings.
Reduce output only under an answer contract
Output reductions can matter when output tokens carry the highest rate. Blind truncation isn't a cost strategy. PolicyOps's cited support answer must state eligibility, cite policy evidence, and name the requester's next step. Compare three formats generated from the same input:
| Candidate | Intended change | Safe for automatic support? |
|---|---|---|
| Verbose | Full explanation with repetition | Correct but unnecessarily long |
| Concise cited | One decision, one citation, one next step | Candidate for release |
| Truncated | Hard stop before citation and action | Reject |
The contract is three booleans, not a token target. Predict the selection before running the cell: the shortest candidate that keeps all three fields should win, while a 55-token answer that drops evidence must be rejected.
1@dataclass(frozen=True)
2class AnswerCandidate:
3 label: str
4 output_tokens: int
5 states_decision: bool
6 cites_policy: bool
7 gives_next_step: bool
8
9def satisfies_answer_contract(candidate: AnswerCandidate) -> bool:
10 return (
11 candidate.states_decision
12 and candidate.cites_policy
13 and candidate.gives_next_step
14 )
15
16candidates = [
17 AnswerCandidate("verbose", 220, True, True, True),
18 AnswerCandidate("concise_cited", 130, True, True, True),
19 AnswerCandidate("truncated", 55, True, False, False),
20]
21safe_candidates = [candidate for candidate in candidates if satisfies_answer_contract(candidate)]
22selected_answer = min(safe_candidates, key=lambda candidate: candidate.output_tokens)
23
24verbose_usage = Usage(input_tokens=2_200, cached_input_tokens=1_280, output_tokens=220)
25concise_usage = Usage(input_tokens=2_200, cached_input_tokens=1_280, output_tokens=selected_answer.output_tokens)
26saved_per_exception = price_usage(verbose_usage).total_usd - price_usage(concise_usage).total_usd
27
28assert selected_answer.label == "concise_cited"
29assert not satisfies_answer_contract(candidates[-1])
30assert saved_per_exception == Decimal("0.001350")
31
32print(f"selected={selected_answer.label}")
33print(f"rejected={candidates[-1].label}")
34print(f"saved_per_exception=${saved_per_exception:.6f}")
35print(f"saved_per_replay_day=${dollars(saved_per_exception * 500)}")1selected=concise_cited
2rejected=truncated
3saved_per_exception=$0.001350
4saved_per_replay_day=$0.68The cheapest candidate is rejected. The concise form saves $0.001350 per exception, or $0.68 across this 500-request slice, while preserving the contract. A compliant shorter answer may also reduce decode time, but verify p95 latency and retry rate instead of inferring either from token count alone.
Batch only work that can wait
Batch is a scheduling choice before it's a pricing choice. OpenAI documents an asynchronous path with 50% lower costs than synchronous APIs and completion within 24 hours.[3] A live customer can't accept that latency contract, but a nightly regression run can.
The pricing page publishes Batch token rates explicitly, so the ledger reads that row instead of deriving every category from one multiplier.[1] In this fixture, the displayed $0.13 cached-input rate means the replay total isn't the exact result of halving every Standard-row decimal locally. Predict the eligibility flags before running the cell: live support stays Standard; nightly evaluation can use Batch.
1@dataclass(frozen=True)
2class Workload:
3 name: str
4 interactive: bool
5 deadline_hours: int
6 endpoint_supported: bool
7 daily_requests: int
8 usage: Usage
9
10def batch_eligible(workload: Workload) -> bool:
11 return (
12 not workload.interactive
13 and workload.deadline_hours >= 24
14 and workload.endpoint_supported
15 )
16
17live_support = Workload(
18 name="live-support-answer",
19 interactive=True,
20 deadline_hours=0,
21 endpoint_supported=True,
22 daily_requests=3_000,
23 usage=Usage(input_tokens=900, cached_input_tokens=0, output_tokens=95),
24)
25nightly_eval = Workload(
26 name="nightly-release-eval",
27 interactive=False,
28 deadline_hours=24,
29 endpoint_supported=True,
30 daily_requests=2_000,
31 usage=Usage(input_tokens=1_800, cached_input_tokens=1_280, output_tokens=80),
32)
33
34nightly_sync_cost = price_usage(nightly_eval.usage).total_usd * nightly_eval.daily_requests
35nightly_batch_cost = price_usage(nightly_eval.usage, batch=True).total_usd * nightly_eval.daily_requests
36
37assert not batch_eligible(live_support)
38assert batch_eligible(nightly_eval)
39assert nightly_batch_cost == Decimal("2.832800")
40assert nightly_batch_cost < nightly_sync_cost
41
42print(f"live_support_batchable={batch_eligible(live_support)}")
43print(f"nightly_eval_batchable={batch_eligible(nightly_eval)}")
44print(f"nightly_eval_sync=${dollars(nightly_sync_cost)}")
45print(f"nightly_eval_batch=${dollars(nightly_batch_cost)}")1live_support_batchable=False
2nightly_eval_batchable=True
3nightly_eval_sync=$5.64
4nightly_eval_batch=$2.83Your nightly evaluation workload is cheaper through Batch. Should the interactive support endpoint use the same path?
Answer
No. The Batch completion window changes latency semantics. Only work that can wait belongs on that path.
Attribute cost to decisions, not models alone
An invoice tells you how much you spent. A release trace tells you why. Each row should name the evaluated release, feature, decision, pricing mode, rate-card identity, token usage, and output-contract evidence.
Recording only a model name can't distinguish an unsafe answer-cache promotion from a harmless increase in live-order traffic. Make contract evidence and rate-card identity mandatory instead of silently repricing old usage with whichever table happens to be loaded.
Sort billed dollars by feature before changing prompts. In this fixture, live-order answers own $11.03 of $23.92, or 46.1%. If a feature crosses a team-defined triage threshold, such as 40% for this release, inspect its volume, output tokens, retries, and tool path first. That threshold prioritizes investigation; it doesn't prove the feature is inefficient.
1@dataclass(frozen=True)
2class LedgerTrace:
3 release_id: str
4 feature: str
5 decision: str
6 requests: int
7 usage: Usage | None
8 rate_card_id: str
9 contract_passed: bool
10 contract_evidence_id: str
11 batch: bool = False
12
13def trace_unit_spend(trace: LedgerTrace, card: RateCard = rate_card) -> Decimal:
14 if trace.rate_card_id != card.rate_card_id:
15 raise ValueError("trace rate card does not match pricing table")
16 if trace.usage is None:
17 return Decimal("0")
18 return price_usage(trace.usage, card, batch=trace.batch).total_usd
19
20def trace_spend(trace: LedgerTrace, card: RateCard = rate_card) -> Decimal:
21 return trace_unit_spend(trace, card) * trace.requests
22
23release_id = "support-release-2026-05-cost-v1"
24optimized_replay = [
25 TrafficSlice(
26 RequestOutcome(
27 feature=item.outcome.feature,
28 decision=item.outcome.decision,
29 usage=concise_usage if item.outcome.feature == "access-exception-answer" else item.outcome.usage,
30 counterfactual_usage=item.outcome.counterfactual_usage,
31 ),
32 requests=item.requests,
33 )
34 for item in daily_replay
35]
36traces = [
37 LedgerTrace(
38 release_id=release_id,
39 feature=item.outcome.feature,
40 decision=item.outcome.decision,
41 requests=item.requests,
42 usage=item.outcome.usage,
43 rate_card_id=rate_card.rate_card_id,
44 contract_passed=True,
45 contract_evidence_id=(
46 "approved-public-policy-cache@v1"
47 if item.outcome.decision == "SEMANTIC_ANSWER_HIT"
48 else "cited-support-answer-v3@canary"
49 ),
50 )
51 for item in optimized_replay
52]
53traces.append(
54 LedgerTrace(
55 release_id=release_id,
56 feature=nightly_eval.name,
57 decision="BATCH_OFFLINE_EVAL",
58 requests=nightly_eval.daily_requests,
59 usage=nightly_eval.usage,
60 rate_card_id=rate_card.rate_card_id,
61 contract_passed=True,
62 contract_evidence_id="nightly-release-eval@batch-eligible",
63 batch=True,
64 )
65)
66
67spend_by_feature: defaultdict[str, Decimal] = defaultdict(lambda: Decimal("0"))
68for trace in traces:
69 spend_by_feature[trace.feature] += trace_spend(trace)
70
71total_with_eval = sum(spend_by_feature.values())
72trace_contracts_complete = all(
73 trace.contract_passed
74 and trace.contract_evidence_id
75 and trace.rate_card_id == rate_card.rate_card_id
76 for trace in traces
77)
78stale_trace = replace(traces[0], rate_card_id="openai-gpt-5.4-short-context-2026-04-30")
79assert spend_by_feature["public-policy-answer"] == Decimal("7.776000")
80assert actual_daily_spend - sum(trace_spend(trace) for trace in traces[:-1]) == Decimal("0.675000")
81assert trace_contracts_complete
82
83for feature in sorted(spend_by_feature):
84 print(f"{feature}=${dollars(spend_by_feature[feature])}")
85print(f"daily_total_with_eval=${dollars(total_with_eval)}")
86print(f"trace_contracts_complete={trace_contracts_complete}")
87try:
88 trace_spend(stale_trace)
89except ValueError as error:
90 print(f"stale_rate_card_rejected={error}")1access-exception-answer=$2.29
2live-order-answer=$11.03
3nightly-release-eval=$2.83
4public-policy-answer=$7.78
5daily_total_with_eval=$23.92
6trace_contracts_complete=True
7stale_rate_card_rejected=trace rate card does not match pricing table
Forecast and gate the release
A release gate answers two separate questions: can we afford this traffic, and can we trust the answers? A cost target alone rewards short, wrong responses. A quality target alone leaves no capacity or margin plan. Require both.
The canary report evaluates the concise cited-answer policy for access exceptions. It projects the replay mix across 30 days and includes the nightly offline evaluation run. Before running it, predict the result: $23.9188 per day becomes a $717.56 monthly forecast, and promotion still requires the quality checks to pass.
1@dataclass(frozen=True)
2class QualityReport:
3 cited_answer_pass_rate: Decimal
4 unsafe_cache_hits: int
5 evaluated_answers: int
6
7@dataclass(frozen=True)
8class PromotionPolicy:
9 monthly_budget_usd: Decimal
10 minimum_cited_answer_pass_rate: Decimal
11 maximum_unsafe_cache_hits: int
12 minimum_evaluated_answers: int
13
14quality = QualityReport(
15 cited_answer_pass_rate=Decimal("0.997"),
16 unsafe_cache_hits=0,
17 evaluated_answers=2_000,
18)
19promotion_policy = PromotionPolicy(
20 monthly_budget_usd=Decimal("750.00"),
21 minimum_cited_answer_pass_rate=Decimal("0.995"),
22 maximum_unsafe_cache_hits=0,
23 minimum_evaluated_answers=2_000,
24)
25monthly_forecast = total_with_eval * Decimal("30")
26budget_headroom = promotion_policy.monthly_budget_usd - monthly_forecast
27uniform_volume_growth_headroom = budget_headroom / monthly_forecast
28
29sample_size_passed = (
30 quality.evaluated_answers >= promotion_policy.minimum_evaluated_answers
31)
32quality_passed = (
33 trace_contracts_complete
34 and sample_size_passed
35 and quality.cited_answer_pass_rate >= promotion_policy.minimum_cited_answer_pass_rate
36 and quality.unsafe_cache_hits <= promotion_policy.maximum_unsafe_cache_hits
37)
38budget_passed = monthly_forecast <= promotion_policy.monthly_budget_usd
39
40assert dollars(monthly_forecast) == Decimal("717.56")
41assert dollars(budget_headroom) == Decimal("32.44")
42assert quality_passed and budget_passed
43
44print(f"monthly_forecast=${dollars(monthly_forecast)}")
45print(f"budget_headroom=${dollars(budget_headroom)}")
46print(f"uniform_volume_growth_headroom={uniform_volume_growth_headroom:.1%}")
47print(f"sample_size_passed={sample_size_passed}")
48print(f"quality_passed={quality_passed}")
49print(f"budget_passed={budget_passed}")1monthly_forecast=$717.56
2budget_headroom=$32.44
3uniform_volume_growth_headroom=4.5%
4sample_size_passed=True
5quality_passed=True
6budget_passed=TrueThis forecast repeats one measured replay day 30 times. The $32.44 remainder represents only 4.5% uniform growth, so replay low, expected, and high volume scenarios before ramping traffic. The hard budget gate passes, but a team could add a soft alert at 90% budget consumption; this fixture is already at 95.7%, so it deserves an owner and a volume plan even with budget_passed=True.

Two routes have the same cost per attempt, but one often needs a retry before producing a valid answer. Which cost should the release decision compare?
Answer
Compare cost per successful answer or task, including retries and failed attempts. Cost per request would hide the extra spend caused by lower reliability.
Produce the next policy input
The next lesson implements model gateway routing and fallbacks. Hand it a budget contract, not a half-built router inside cost analysis. The contract says what every future route must preserve: citation schema, an approved generated-answer ceiling, and the release evidence behind that limit.
Set the ceiling as a policy value that the observed maximum fits under. Treat it as a lane-admission limit for one successful generation, not a bound on total request spend. A failed primary can bill before timeout, and a contract-preserving fallback can bill again. Incident TCO can exceed maximum_generated_answer_usd when attempts stack, so the gateway lesson must track request-level spend separately.
Before running the final cell, predict the handoff: the observed maximum is $0.004570, the policy ceiling is $0.005000, and the status can promote only because quality and budget gates already passed.
1@dataclass(frozen=True)
2class GatewayBudgetContract:
3 release_id: str
4 required_answer_schema: str
5 maximum_generated_answer_usd: Decimal
6 monthly_forecast_usd: Decimal
7 rate_card_id: str
8 status: str
9
10observed_max_generated_cost = max(
11 trace_unit_spend(trace)
12 for trace in traces
13 if trace.usage is not None and not trace.batch
14)
15per_answer_budget_usd = Decimal("0.005000")
16assert observed_max_generated_cost <= per_answer_budget_usd
17
18status = "PROMOTE_COST_POLICY" if quality_passed and budget_passed else "HOLD_RELEASE"
19gateway_contract = GatewayBudgetContract(
20 release_id=release_id,
21 required_answer_schema="cited-support-answer-v3",
22 maximum_generated_answer_usd=per_answer_budget_usd,
23 monthly_forecast_usd=dollars(monthly_forecast),
24 rate_card_id=rate_card.rate_card_id,
25 status=status,
26)
27
28assert gateway_contract.status == "PROMOTE_COST_POLICY"
29assert observed_max_generated_cost == Decimal("0.004570")
30assert gateway_contract.maximum_generated_answer_usd == Decimal("0.005000")
31
32print(f"status={gateway_contract.status}")
33print(f"required_schema={gateway_contract.required_answer_schema}")
34print(f"observed_max_generated_answer=${observed_max_generated_cost:.6f}")
35print(f"max_generated_answer=${gateway_contract.maximum_generated_answer_usd:.6f}")
36print("next_step=apply_contract_in_gateway_routing")1status=PROMOTE_COST_POLICY
2required_schema=cited-support-answer-v3
3observed_max_generated_answer=$0.004570
4max_generated_answer=$0.005000
5next_step=apply_contract_in_gateway_routingWhat the ledger proves
By now, "cache hit" isn't an explanation by itself. Each claim about this release needs the receipt that supports it:
| Evidence | Correct conclusion |
|---|---|
| Semantic answer hit with valid release scope | New generation was avoided for an already approved answer class |
Provider cached_tokens on a generated answer | Repeated prefix processing was charged at the cached-input rate |
| Concise cited-answer candidate passes evaluation | Output reduction is eligible for release |
| Nightly evaluation classified as noninteractive | Batch discount can be modeled without changing customer latency |
| Every usage trace pins the loaded rate-card identity | Historical spend remains reproducible instead of being silently repriced |
| Monthly forecast and quality check pass | Cost policy can be promoted and handed to gateway design |
Wait for usage traces before claiming a prompt rewrite improved pricing through cached tokens. Count semantic-cache savings only while accepted-hit quality remains valid. Never approve a shorter output merely because it costs less.