Per-step cost and latency
Token counts are emitted per model call, prices live on a vendor page that changes, and the component that renders a dollar figure has to be honest about which of those two it is actually showing.
A reviewer opens the trace panel and asks the cheapest possible question: what did that step cost, and how long did it take? Both look like the same kind of number. They are not. One is read off a record the runtime emitted. The other is read off a web page maintained by a company that can change it on a Tuesday.
This lesson is about keeping those two apart in the component, because a panel that renders them in the same typeface is asserting a confidence it does not have about half of what it shows.
Two numbers, two provenances
Module one ended with a schema where every field carried a provenance tag. This is where that tag pays for itself. Sort the numbers on a cost row into two piles:
- Emitted. Token counts, start timestamps, end timestamps. These came out of the run. They are as true as the instrumentation was careful.
- Looked up. Price per token. This never appears in a span. It is a fact about a vendor’s commercial policy on a particular date, which you fetched and stored.
A dollar figure is a product of the two. It inherits the uncertainty of both, and it inherits the staleness of the second entirely. That is the whole lesson, and the rest is mechanism.
The emitted half: tokens
The GenAI semantic conventions put usage on the model-call span: gen_ai.usage.input_tokens, gen_ai.usage.output_tokens, gen_ai.usage.cache_read.input_tokens and gen_ai.usage.cache_write.input_tokens, in the spans conventions as read on 2026-09-05 from main. Those pages are marked Status: Development and the repository has no tagged release, so those four names are dated, not settled — the same caveat the conventions lesson established, and it has not stopped being true.
The counts themselves are per model call, which means a run’s total is something you sum, not something you read. There is no run-level token field to fetch. If your component shows a run total, it computed it, and it should be able to say from how many spans.
The identity a naive cost function gets wrong
Caching breaks the obvious arithmetic. Anthropic’s prompt-caching page — Anthropic documenting its own product — gives the usage object in literal form and states the part that matters: input_tokens counts only the tokens after the last cache breakpoint. So the true input total is:
total_input_tokens
= cache_read_input_tokens
+ cache_creation_input_tokens
+ input_tokensMultiply input_tokens alone by a base input rate and you are wrong twice in the same expression: the quantity is short by everything the cache covered, and the rate is wrong for the tokens you did omit, because cached reads and cache writes are priced on their own multipliers. Two errors pointing in opposite directions produce a figure that looks plausible, which is worse than one that looks absurd.
The emitted half: time
Duration is usually not a field. The span model gives you a start and an end timestamp and expects you to subtract. LangSmith — LangChain documenting its own hosted product — is explicit about the same gap in its run data format: there is no duration column, only start_time and end_time, plus a separate first_token_time that is time-to-first-token rather than total time. Those are two different latencies and a reader cares about both for different reasons: first token is when the interface stopped looking broken, total time is when the work finished.
The trap is one level up. Step durations do not sum to run duration. The conventions’ own non-normative examples note that tool and model spans are likely to be siblings rather than parent and child, and siblings can overlap. Add up the step times of a run that fanned out three tool calls in parallel and you will report more elapsed time than the wall clock did. Wall-clock duration is root end minus root start. Summed step time is a different quantity — useful, but it is machine time, not waiting time, and the label has to say which one it is.
The looked-up half: price, and how fast it moves
Here is the reason this lesson refuses to print a rate card. On 2026-09-05, Anthropic’s pricing page — the vendor stating its own prices — carried this note:
“The $2/$10 per million input/output token pricing for Claude Sonnet 5, announced at launch as introductory pricing through August 31, 2026, is now the standard price. The previously scheduled increase to $3/$15 per million input/output tokens on September 1, 2026 will not occur.” (Claude pricing, fetched 2026-09-05)
Read what that describes. A price change was announced, dated, and scheduled. It then did not happen, and the page announcing the reversal went up within days of the date it was supposed to take effect. This course’s own resource file recorded the same page three days earlier and would have been wrong about the future by the time you read it.
So a hardcoded price is not merely something that decays slowly. It can be falsified by a page edit you never see, in either direction, and nothing in your component will notice. Neither will your reviewer, because a wrong dollar figure looks exactly like a right one.
Two structural facts from that page outlive any of its numbers, and they are the ones worth carrying. First, a model has more than one rate: input, output, cached read, and cache write at more than one cache lifetime, with batch processing on its own discount. A cost function with one rate per model cannot express the run it is pricing. Second, the page notes that Claude 4.7 and later models use a newer tokenizer producing roughly 30% more tokens for the same text (read on the same page, recorded 2026-09-02). A lower per-token price therefore does not mechanically mean a cheaper run, and a token-count chart that spans a model upgrade is partly a chart of the tokenizer.
The same shape holds across vendors. OpenAI’s pricing page — again, a vendor stating its own prices — lists input, cached input and output separately per model, and the roster of models on it changed between this course’s two fetches three days apart. Do not build a component around “the current flagship.” That title has a shelf life measured in days.
The number that ages badly
If you take one implementation rule from this lesson: a price in your codebase is data with a fetch date attached, never a literal in a cost function. Store the date next to the rate, render the date next to the figure, and give the component a real state for “no rate on file for this model.” That state is not a failure. It is the honest answer, and it is the one your panel will need the first time a run uses a model you have never priced.
The arithmetic, and the part of it that survives
Prices go stale. The shape of the calculation does not. Write the shape and let the rate card be an input:
type Usage = {
input: number
output: number
cacheRead: number
cacheWrite: number
}
type Rate = {
model: string
inputPerMTok: number
outputPerMTok: number
cachedReadPerMTok: number
cacheWritePerMTok: number
/** The day this was read off the vendor's page. Not optional. */
fetchedOn: string
sourceUrl: string
}
type StepCost =
| { kind: 'priced'; usd: number; rate: Rate }
| { kind: 'unpriced'; reason: 'no-rate-on-file' | 'no-usage-emitted' }
// Every term is a separate multiplication because every term
// has its own rate. Collapsing them is the bug.
const usd = (u: Usage, r: Rate) =>
(u.input * r.inputPerMTok +
u.output * r.outputPerMTok +
u.cacheRead * r.cachedReadPerMTok +
u.cacheWrite * r.cacheWritePerMTok) /
1_000_000Note what the union buys you. unpriced is a first-class result, not a zero and not a dash. A zero is a lie about a run that cost money; a dash is a shrug. Naming the reason lets the panel say no rate on file for this model versus this span emitted no usage attributes, which are two different problems with two different fixes, and the second one is a bug in the instrumentation that the panel is now surfacing for free.
Retrieval check
Your run summary shows “Total: 4.2s” and the step rows below it show durations adding to 9.7s. A reviewer says the summary is broken. Is it?
Check your answer
Probably not. Those are two different quantities. If any of the steps ran concurrently — and the conventions expect tool and model spans to be siblings under an encompassing span, which is exactly the arrangement that permits overlap — then wall-clock time is less than summed step time by however much they overlapped. Root end minus root start is the elapsed time a human waited. The sum of the children is total machine time spent.
The bug is not the arithmetic. It is that both numbers were rendered as if they answered the same question. Label one elapsed and the other compute time across steps, and the reviewer who spotted the discrepancy has learned something about the run instead of filing a ticket against your panel.
Check your recall
Answer from memory — no scrolling back.
Hands on
Give the cost column a provenance
Done when: ARTIFACT.md’s Cost provenance field names the vendor page you read, the date you read it, the rates you stored, and what the component renders when no rate is on file — and a run in your captured payload produces either a priced figure or a named unpriced state, with nothing in between.
- Fetch the rate card yourself, today, from the vendor page for whatever model your captured run used. Do not copy the numbers out of this lesson or out of
RESOURCES.md. Both were true on a date that has already passed. - Store it as data with a
fetchedOndate and the source URL, in one file, separate from any component. If the date is not there, the entry is not finished. - Write the cost function with a term per rate, and a return type that can say
unpricedwith a reason. Test it against a model name you deliberately left out of the rate card, and confirm the panel renders the unpriced state rather than a zero. - Compute two durations for the run: root end minus root start, and the sum of the leaf spans. Write both down. If they differ, you have found concurrency in your own payload, and your step rows now need labels that distinguish elapsed from compute time.
- Check your captured payload for the cache fields. If they are absent, record that in
ARTIFACT.mdas a property of that runtime rather than assuming caching was off — an attribute that was never emitted and a value that was genuinely zero look identical from where you are standing. - Bring the rate-card file and the two duration figures into the chat. The first thing worth checking is whether every stored rate carries a date, because that is the field a hurry deletes.
What this does not cover
This lesson stayed on one run: the cost and latency of the steps in front of you, and where each figure came from. It says nothing about the question the person paying the bill is actually asking, which is about every run rather than this one — what a typical run costs, how that has moved this month, which tool is responsible for the tail. That is a different data shape and a different signal, and it is the roll-up lesson, which comes next.
It also left out everything about what the run changed. Cost and time tell a reviewer what a run consumed, not what it did to the world, and the diff view lesson closes the course on that.
Read this next — primary source
Claude pricingAnthropic — vendor stating the prices for its own product; free. Fetched 2026-09-05; the page carries no date stamp of its own.
This lesson takes one quotation and one structural fact from this page: that a published price can be scheduled to change and then not change, and that a rate card has more than one rate per model. Read it in full for the shape of a real rate card rather than for its current numbers, which will have moved by the time you get there. Notice how many separate dimensions carry their own multiplier — cache reads, cache writes at two lifetimes, batch processing, tool use billed per call rather than per token — because every one of those is a term your cost function either models or silently gets wrong. Then read it a second time asking which of those dimensions your runtime even emits a token count for.
Stuck, curious, or think this lesson is wrong? Ask your teaching agent. The lessons are the scaffold; the conversation is where the learning gets unstuck.