Chapter 2 · section 4 of 5
In production
The deployed version of this call, learnerRequestFor in worker/src/learner-ask.ts, differs from yours in several ways, three of which are worth seeing.
system: [
{ type: 'text', text: prompt.rules },
// The book is the stable prefix and the conversation goes after it,
// so a follow-up reads the cache rather than rebuilding it.
{ type: 'text', text: prompt.sources, cache_control: { type: 'ephemeral' } },
], The system prompt carries a book. The deployed box answers two kinds of question: road law, from retrieved laws in the user turn as yours does, and questions about the app itself, from a description of how the app works that rides in the system prompt on every request. That description runs to a few thousand tokens, which is where the cache_control comes from.
Prompt caching is the API keeping a copy of the beginning of a request so that the next request with the same beginning is charged a tenth of the price for that part. It matches on a prefix: every byte up to the marked block has to be identical, which is why the rules and the book go first and never change, and the laws, which change with every question, go in the user turn after them. The deployed pipeline’s design document estimated an app question at about a third of a cent with the book cached; a road-law question, which adds the rephraser of Chapter 6 and the choosing stage of Chapter 7 before an answer over eight laws, measured about 1.5 cents on 10 October 2026. The minimum prefix the API will cache is 512 tokens on the model you are using; your five rules are shorter than that, which is why your listing carries no cache_control. A marker on a prefix too short to cache is not an error, just a cache_read_input_tokens of zero on every request, and the only way to know is to print the field.
The rules are in two languages. A learner who asks in Spanish is answered in Spanish from the English corpus, under a Spanish version of the road-law rule, because the English rule with Spanish trigger words quoted inside it sent the Spanish no-licence question back empty four times in six. The chooser is one line:
export function rulesFor(lang: Lang): Record<keyof typeof RULES, string> {
return lang === 'es' ? { ...RULES, roadLaw: ROAD_LAW_ES } : RULES
} A seventh rule exists and is sent only when it applies. When a question is asked across every state at once, a rule about comparing states is appended. It is not in RULES, so that a one-state question’s prompt is byte for byte the prompt the measurements were taken against; a rule that applies only when fifty states’ laws are given would be one more sentence for each of the other questions to misread. Your fourth rule, on the fine schedule and the state’s own page, is kept out of RULES for the same reason: the deployed pipeline appends each half to the user turn, after the laws, only when a row of that kind is among them, and the cached prefix is the same bytes either way.