Today at a glance
- Recall10 min
- Concept · Tokens and context55 min
- Build · First Claude call70 min
- Drill · JSON folder summary25 min
- Job search20 min
Yesterday the repo could build and the secret scan could fire, and no model had been called. Today you learn the unit the API actually bills and limits — the token — and you send one Messages API request through a client you write by hand. The call logs its stop reason, its input and output token counts, and a cost computed from prices you looked up. That log is the first number SWIFTCLAIM is allowed to put under Measured.
Recall (10 min, no notes)
Tuesday’s recall is Python, taken from the hygiene tests you wrote yesterday. Answer aloud, then open each one.
1 · The test file is tests/test_repo_hygiene.py. Which directory is Path(__file__).resolve().parents[1]?
parents[0] is the tests/ directory. parents[1] is its parent, the repository root, which is why the tests read .env.example and .gitignore from there.
2 · What does REQUIRED_KEYS <= read_env_example().keys() demand?
On sets, <= means “is a subset”. Every required key must be present. Extra keys are allowed. A missing key fails the test.
3 · Why does the API-key test require the value "", rather than “does not look like a real key”?
.env.example is committed. The value that is safe to commit is empty. A shape check still lets a real key through when the key has an unusual shape, and it trains you to commit key-shaped strings.
4 · A value contains =, as a URL or a password might. What does partition("=") keep that split("=") would break apart?
partition returns three pieces: the key, the first separator, and the entire remainder, so further = characters stay in the value. split("=") with no limit cuts on every equals sign, and unpacking into three variables then fails or drops the tail.
5 · Why did uv init --package matter for import swiftclaim?
It creates a src/swiftclaim layout, so pytest imports the installed package. A loose module sitting in the working directory can pass on your machine and fail once the package is installed somewhere else.
6 · Name one way test_dotenv_is_gitignored passes while a secret still reaches GitHub.
The test only checks that the line .env appears in .gitignore. A file committed before that line was added stays tracked, git add -f bypasses the ignore rule, and a hook that was never installed will not block a key pasted into some other file. Yesterday’s planted-token commit is what proves the hook.
Concept · Tokens, context windows, and what a call costs (55 min)
The model continues a sequence
A message to Claude is cut into tokens before the model sees it. A token is a chunk of text from the model’s vocabulary: sometimes a whole word, sometimes part of one, sometimes punctuation or whitespace. Words, characters and tokens are three different counts. Claude 4.7 and later use a newer tokenizer that produces more tokens for the same text than earlier Claude models (about 30% more, and the exact increase depends on the text — pricing page). A fixed “characters per token” ratio copied from an old blog post will mis-bill you. The count that matters is the one the API reports.
Given the tokens so far, the model assigns a probability to every token that could come next, one token is chosen, and that token is appended. Then it happens again. The reply is that sequence. Generation stops when the model finishes the turn, when it hits a limit you set, or when it hits a limit the API enforces. Nothing in that loop looks up a claims database. A reply that reads as finished has stopped generating. It has not been checked against a record. Thursday is when that failure gets its proper name; today, remember it when you read stop_reason.
prompt (input tokens) reply (output tokens)
[ system | messages | tools ] ──prefill──▶ t → t → t → t → stop
billed as input one token at a time
billed as output
context window = working memory for THIS request
| system + every message + tools + this turn's output, including thinking |
The context window is this request’s working memory
The context window is every token the model can refer to while generating, including the reply it is writing. It is the request’s working memory. The training corpus is a different thing, and a knowledge cutoff (Thursday) is a different thing again.
Checked against the context windows page on 29 Oct 2026: current Claude models, including Haiku 5.5, Sonnet 5.5 and Opus 5.5, have a 1M-token window, and one synchronous Messages request can generate up to 128k output tokens. Confirm the row for the model you call; older models in that same page are at 200k. A larger window holds a longer prompt. It does not make a long prompt more accurate. The same page names the degradation as context rot: as the token count grows, accuracy and recall drop, so what you choose to include matters as much as the size of the window.
| Piece | Counts toward the window? | How it is billed on an uncached call |
|---|---|---|
| System prompt | Yes | Input |
| Every message, including tool results, images and documents | Yes | Input |
| Tool definitions | Yes | Input |
| This turn’s output, including thinking | Yes | Output |
| A cached prefix, when you use caching later | Yes — caching changes the price, and the tokens still occupy the window | Split across input_tokens, cache_creation_input_tokens and cache_read_input_tokens. Add all three. |
Two different overflows, from that same page:
- The input alone exceeds the window: every model returns HTTP 400
invalid_request_error(“prompt is too long”). - On Claude 4.5 and newer, input plus the
max_tokensyou requested may exceed the window and the API still accepts the call. If generation then fills the window,stop_reasonismodel_context_window_exceeded. Treat that text as truncated.
Estimate a request before you send it with the token counting endpoint, POST /v1/messages/count_tokens. The usage object on the response is what you were billed.
Why cost and latency grow with tokens
Input tokens and output tokens are priced separately, per million tokens, and the output rate on the current price table is higher than the input rate for the same model. A longer prompt costs more because it is more input. A longer answer costs more because it is more output. max_tokens is a ceiling on how many tokens may be generated. It is a budget, and the model may stop earlier. You pay for tokens produced, which usage.output_tokens reports.
Latency follows the same split, for a mechanical reason. The reply is produced one token at a time, each conditioned on the tokens already chosen, so a longer answer is more generation steps. A longer prompt adds prefill work as well as input cost. Thinking, when the model does it, is extra output: it counts toward max_tokens, it counts toward the window, and it is billed as output. The models overview states the practical consequence: latency depends on prompt length, output length and thinking effort. There is no single milliseconds-per-token figure worth memorising.
usage.output_tokens is the billed total. It includes thinking tokens, and the API’s own docs say the counts do not match the visible text one-to-one — an empty-looking reply can still report a non-zero output_tokens. Log the usage fields. Counting characters in text will understate the bill.
Worked example
A smoke call on Claude Haiku 5.5 (claude-haiku-5-5), prompt under 100,000 tokens, no cache. Rates from the pricing page on 29 Oct 2026: $0.10 per million input tokens and $0.50 per million output tokens. Prompts over 100,000 tokens on this model use a higher row ($0.50 and $2.50). Other current 1M-window models stay on their standard rates. Re-read the page on the day you run the call and write that date next to the rates you multiply. If the row has moved, use the new row; the arithmetic below is here so you can check your own multiplication.
Tokens from usage | Rate | USD | |
|---|---|---|---|
| Input | 800 | $0.10 / million | 800 × 0.10 / 1,000,000 = 0.00008 |
| Output | 120 | $0.50 / million | 120 × 0.50 / 1,000,000 = 0.00006 |
| Call | 0.00014 |
Those 120 output tokens might be a short visible reply plus thinking. You still multiply 120. If this same prompt were 150,000 tokens, Haiku 5.5’s over-100k row would apply to the whole prompt. Look up that threshold rather than assuming every 1M-window model has one.
One Messages API call
The call you will write hits POST /v1/messages through the official Python SDK. The fields this week cares about:
model— a pinned id. For today’s smoke call useclaude-haiku-5-5, the current high-volume model, after you confirm the id on the models page. Week 2 compares a small tier with a mid tier; today one model is enough.max_tokens— required. The ceiling on generation. Thinking tokens spend this budget before you see text, so a tiny ceiling can come back asstop_reason: "max_tokens"with little or no visible text. The official examples use 1024. Use that for the smoke call.system— the top-level system prompt: role and standing instructions. Pass it as its own argument.messages— the turns. Today that is one user message:{"role": "user", "content": "..."}.
The response carries content (a list of blocks), stop_reason, model and usage. Text you show a human comes from blocks with type == "text". Thinking blocks are a different type; leave them out of the string you store as the answer, and leave them inside the output-token count, where the API already put them.
stop_reason | What happened | What you do today |
|---|---|---|
end_turn | The model completed the turn. | Use the text. Completing the turn means it stopped. It does not mean the content is true. |
max_tokens | Generation hit the ceiling you set. | Log it. The text may be cut off mid-structure. Saturday’s Break-It lab is where you detect this and retry or route to a human. Today you only record it. |
model_context_window_exceeded | Generation filled the window. | Same as a truncated answer: log it, do not treat the text as complete. |
stop_sequence | A string you listed in stop_sequences was emitted. | You are not sending stop sequences today. |
tool_use | The model wants a tool to run. | Week 2. Today you send no tools, so you should not see this. |
refusal | The model declined. stop_details names the category. | Log it. A smoke prompt that asks for the word “ok” should not hit this. |
pause_turn | A server-side tool loop hit its iteration limit. | You are not using server tools today. |
Full list and the recommended handling: stop reasons.
temperature, top_p and top_k are rejected on Claude 4.7 and later, and on every model when the API version is 2026-10-21: the call returns 400. Omit them. Sampling as an idea is tomorrow’s concept; the current API wants you to steer with the prompt. The Python SDK chooses the version header. If you ever call curl instead, copy the headers from the current Messages docs.
Seven questions
| What is it? | A token is the unit the model reads and writes. The context window is how many of those units this request may hold, prompt and reply together. usage is the count you were billed. |
|---|---|
| Why does it exist? | The model continues a sequence one token at a time, and the provider’s cost and limits are defined on that unit. A character count will not match the invoice or the error you get when a prompt does not fit. |
| How does it work? | The API tokenises the system prompt, messages and tools, runs prefill, then generates until end_turn or a limit. You read stop_reason and usage, and you multiply input and output counts by the rates you looked up. |
| When would I use it? | On every model call: pick a max_tokens that can finish (including thinking), log the usage, and price it from the current table. Use the count-tokens endpoint before you send a document you have not measured. |
| Alternatives? | Estimating from a character ratio (drifts across tokenizers). Hard-coding a price in the client (goes stale). Setting max_tokens to 1 to save money (the ceiling cuts the answer, and thinking may consume it before any text). Stuffing the whole file in because the window is large (you pay for it, and long context recalls worse). |
| What can go wrong? | A 400 because the prompt does not fit, or because sampling parameters were sent. A max_tokens stop that looks like a short answer and is actually a cut-off answer. A bill that ignores thinking tokens or cache fields. A price row for the wrong model, or Haiku’s over-100k row applied when the prompt is small — or missed when it is large. |
| Explaining the trade-off to a client | “We pay for every token we send and every token the model writes, including reasoning you may not see in the answer. A longer policy costs more and, past a point, the model uses it worse. I would rather send the relevant pages and show you the token log than paste the whole file.” |
How this shows up in SWIFTCLAIM today
The smoke call is the first measured number in the project. The client you write returns the stop reason and the token counts; a small script multiplies them by the rates you checked and appends one JSON line. The README’s Measured section stays empty of business claims — one smoke call does not say anything about triage quality — and it can record that a call was logged. The prompt contains no claim text and no personal data. Synthetic data only, and the API key stays in .env.
Build · First Claude call (70 min)
NO AI on src/swiftclaim/llm.py. Rule 2: through Week 3 the LLM client is hand-written. AI may review the module after the tests pass. It may not write the body. The test file and the smoke script below are the spec and the wiring; copy them.
-
Add the SDK inside the SWIFTCLAIM repo from yesterday.
uv add anthropicThe SDK reads
ANTHROPIC_API_KEYfrom the environment. Put the real key in.envonly. Leave.env.exampleempty. -
Create
src/swiftclaim/llm.pywith this stub. You will replace the twoNotImplementedErrorbodies. Leave the signatures and the docstrings."""Hand-written Messages API wrapper. Do not generate this file. AI may review it after the tests pass. It may not write the body. """ from dataclasses import dataclass @dataclass(frozen=True) class Completion: """One Messages API response, reduced to what the call log needs.""" text: str stop_reason: str input_tokens: int output_tokens: int model: str def cost_usd(self, input_usd_per_million: float, output_usd_per_million: float) -> float: """USD for this uncached call at the prices passed in. input_tokens * input_usd_per_million / 1_000_000 + output_tokens * output_usd_per_million / 1_000_000 Cache tokens are not in these two counts. Today's calls are uncached, so usage.input_tokens is the whole input bill. """ raise NotImplementedError def complete( client, *, model: str, system: str, user: str, max_tokens: int, ) -> Completion: """Send one user turn. Return text, stop reason and token counts. ``client`` is an ``anthropic.Anthropic`` instance. Tests pass a fake with the same shape: ``client.messages.create(...)`` returns an object whose ``content``, ``stop_reason``, ``model`` and ``usage`` match the API. Pass ``system`` through as a string. Do not send temperature, top_p or top_k. ``text`` is the concatenation, in order, of content blocks whose type is ``"text"``. Copy stop_reason, model, usage.input_tokens and usage.output_tokens as returned. Do not retry: a max_tokens stop is still a successful return value. """ raise NotImplementedError -
Create
tests/test_llm.pyand run it. It must fail onNotImplementedErrorbefore you write any logic. That is the point of writing the test first."""Tests for the hand-written Messages API wrapper. No network.""" from types import SimpleNamespace import pytest from swiftclaim.llm import Completion, complete class FakeMessages: def __init__(self, response): self.response = response self.calls = [] def create(self, **kwargs): self.calls.append(kwargs) return self.response class FakeClient: def __init__(self, response): self.messages = FakeMessages(response) def message(*, text, stop_reason, input_tokens, output_tokens, model="claude-haiku-5-5", extra_blocks=()): return SimpleNamespace( model=model, stop_reason=stop_reason, content=[*extra_blocks, SimpleNamespace(type="text", text=text)], usage=SimpleNamespace(input_tokens=input_tokens, output_tokens=output_tokens), ) def test_complete_sends_one_user_turn_and_copies_usage(): client = FakeClient( message(text="ok", stop_reason="end_turn", input_tokens=20, output_tokens=5) ) result = complete( client, model="claude-haiku-5-5", system="You are a claims-triage assistant.", user="Smoke test.", max_tokens=1024, ) sent = client.messages.calls[0] assert sent["model"] == "claude-haiku-5-5" assert sent["max_tokens"] == 1024 assert sent["system"] == "You are a claims-triage assistant." assert sent["messages"] == [{"role": "user", "content": "Smoke test."}] assert "temperature" not in sent assert "top_p" not in sent assert "top_k" not in sent assert result == Completion( text="ok", stop_reason="end_turn", input_tokens=20, output_tokens=5, model="claude-haiku-5-5", ) def test_complete_keeps_text_blocks_only_in_order(): response = message( text="tail", stop_reason="end_turn", input_tokens=1, output_tokens=2, extra_blocks=( SimpleNamespace(type="thinking", thinking="hidden"), SimpleNamespace(type="text", text="head "), ), ) result = complete(FakeClient(response), model="m", system="s", user="u", max_tokens=8) assert result.text == "head tail" def test_complete_returns_empty_text_when_only_thinking_came_back(): response = SimpleNamespace( model="m", stop_reason="max_tokens", content=[SimpleNamespace(type="thinking", thinking="budget spent here")], usage=SimpleNamespace(input_tokens=10, output_tokens=10), ) result = complete(FakeClient(response), model="m", system="s", user="u", max_tokens=10) assert result.text == "" assert result.output_tokens == 10 assert result.stop_reason == "max_tokens" def test_max_tokens_stop_is_returned_without_a_retry(): client = FakeClient( message(text='{"summary":', stop_reason="max_tokens", input_tokens=100, output_tokens=16) ) result = complete(client, model="m", system="s", user="u", max_tokens=16) assert result.stop_reason == "max_tokens" assert result.text == '{"summary":' assert len(client.messages.calls) == 1 def test_cost_uses_input_and_output_rates_separately(): completion = Completion( text="ok", stop_reason="end_turn", input_tokens=2_000, output_tokens=500, model="claude-haiku-5-5", ) # Fixture rates, not a live price: 2000/1e6*2 + 500/1e6*10 = 0.009 assert completion.cost_usd(2, 10) == pytest.approx(0.009)uv run pytest tests/test_llm.py -q -
Make the tests pass. Open the hints only when you are stuck, and close them before you type.
Hint · the request
Call
client.messages.createwith keyword argumentsmodel,max_tokens,systemandmessages.messagesis a one-element list: role"user", content equal to theuserstring. Omit every sampling parameter, including ones you would set to a default.Hint · the response
Blocks are objects, not dicts. Keep a block when
block.type == "text", and readblock.text. Join those strings with""so"head "and"tail"become"head tail". A thinking block has no.textyou should touch. Readresponse.usage.input_tokensandresponse.usage.output_tokenseven whentextis empty.Hint · the cost
The docstring is the formula. Both counts are divided by one million, then multiplied by their own rate. The rates in the test (2 and 10) are fixtures so the test does not rot when the price page changes. The smoke script is where the real rates go.
-
Log one real call. Copy
scripts/first_call.py. Fill the two rates andPRICE_CHECKEDfrom the pricing page before you run it. Confirm the model id on the models page in the same sitting."""One real Messages API call. Run after tests/test_llm.py passes. set -a && source .env && set +a uv run python scripts/first_call.py """ import json import os from datetime import datetime, timezone from pathlib import Path import anthropic from swiftclaim.llm import complete MODEL = "claude-haiku-5-5" # confirm on the models page the day you run this MAX_TOKENS = 1024 # Fill from https://platform.claude.com/docs/en/about-claude/pricing # Haiku 5.5 has a higher row once the prompt exceeds 100_000 tokens. INPUT_USD_PER_MILLION = None OUTPUT_USD_PER_MILLION = None PRICE_CHECKED = "" # YYYY-MM-DD SYSTEM = ( "You are a claims-triage assistant for a fictional insurer. " "Personal project, synthetic data. Reply with the single word ok." ) USER = "Smoke test. Do not invent a claim." def main() -> None: if ( INPUT_USD_PER_MILLION is None or OUTPUT_USD_PER_MILLION is None or not PRICE_CHECKED ): raise SystemExit("Fill the two rates and PRICE_CHECKED from the pricing page first.") if not os.environ.get("ANTHROPIC_API_KEY"): raise SystemExit("ANTHROPIC_API_KEY is missing. Source .env in this shell.") completion = complete( anthropic.Anthropic(), model=MODEL, system=SYSTEM, user=USER, max_tokens=MAX_TOKENS, ) record = { "at": datetime.now(timezone.utc).isoformat(), "model": completion.model, "stop_reason": completion.stop_reason, "input_tokens": completion.input_tokens, "output_tokens": completion.output_tokens, "text": completion.text, "cost_usd": completion.cost_usd(INPUT_USD_PER_MILLION, OUTPUT_USD_PER_MILLION), "input_usd_per_million": INPUT_USD_PER_MILLION, "output_usd_per_million": OUTPUT_USD_PER_MILLION, "price_checked": PRICE_CHECKED, } log_path = Path("logs/calls.jsonl") log_path.parent.mkdir(exist_ok=True) with log_path.open("a") as handle: handle.write(json.dumps(record) + "\n") print(json.dumps(record, indent=2)) if completion.stop_reason != "end_turn": print(f"stop_reason={completion.stop_reason}: logged, not retried.") if __name__ == "__main__": main()printf 'logs/\n' >> .gitignore set -a && source .env && set +a uv run python scripts/first_call.py uv run pytest -q uv run ruff check .What “good” looks like on the first real callstop_reasonisend_turnandtextis a short acknowledgement.output_tokensmay be larger than that word, because thinking is billed as output. If you seemax_tokens, the budget was spent before the reply finished: say so in the log line and leave the retry for Saturday. If you see 400, read the error. Sampling parameters and an over-long prompt are the two causes this lesson prepared you for. -
Commit the code, not the secret and not the log.
git statusshould showllm.py,tests/test_llm.py,scripts/first_call.py,pyproject.tomlanduv.lock. It should not show.envorlogs/. The prices live in the script because you set them; that is fine. The key must not.git add -A && git status git commit -m "Log a first Claude call: tokens, stop reason, cost"Run the secret-scan hook on the way in. If it does not run, stop and fix yesterday’s install before you commit.
Acceptance criteria
uv run pytest -qpasses, including the five tests that never touch the network.src/swiftclaim/llm.pywas written by you. You can point at the line that buildsmessagesand the line that readsusage.- One real call is appended to
logs/calls.jsonlwith model id,stop_reason,input_tokens,output_tokens,cost_usdand the date you checked the price. - The rates in that log match the pricing page for that model and prompt size, and you can recompute
cost_usdby hand from the two token counts. .envandlogs/are gitignored..env.examplestill has an empty API key.
Drill · Load JSON, validate, summarise (25 min)
NO AI 25 min Python, standard library only. About 20–40 lines. Narrate the invalid cases before you run them.
Problem
Write a program that reads every *.json file in a directory (not subdirectories). Each file should hold one object:
id— non-empty stringcategory— non-empty stringamount— an int or a float, zero or greater. A JSON boolean is invalid even thoughboolis a subclass ofintin Python.status—"open"or"closed"
A file that is not valid JSON, or whose object fails a rule, counts as invalid. The program keeps going. Ignore files that are not .json.
Print exactly these lines. With no valid records, the category line and the three amount lines say n/a.
files: 4
valid: 2
invalid: 2
by_category: auto=1, property=1
amount_min: 100
amount_max: 2500
amount_mean: 1300.0
Categories are sorted by name. amount_mean is the arithmetic mean of the valid amounts.
Fixtures
c1.json: {"id": "c-001", "category": "auto", "amount": 100, "status": "open"}
c2.json: {"id": "c-002", "category": "property", "amount": 2500, "status": "closed"}
bad.json: {"id": "", "category": "auto", "amount": 10, "status": "open"} — invalid, empty id.
weird.json: {"id": "c-003", "category": "auto", "amount": true, "status": "open"} — invalid, boolean amount.
notes.txt sits in the same folder and must not affect files.
Hint
Check isinstance(amount, bool) before isinstance(amount, (int, float)). True would otherwise pass the numeric test, because bool subclasses int. Path.glob("*.json") skips notes.txt. Catch json.JSONDecodeError per file so one broken file does not kill the run.
Reference solution — open only after the timer
import json
from pathlib import Path
REQUIRED = ("id", "category", "amount", "status")
STATUSES = {"open", "closed"}
def is_valid(record: object) -> bool:
if not isinstance(record, dict):
return False
if any(key not in record for key in REQUIRED):
return False
if not isinstance(record["id"], str) or not record["id"].strip():
return False
if not isinstance(record["category"], str) or not record["category"].strip():
return False
amount = record["amount"]
if isinstance(amount, bool) or not isinstance(amount, (int, float)) or amount < 0:
return False
return record["status"] in STATUSES
def summarise(folder: Path) -> None:
files = sorted(folder.glob("*.json"))
valid: list[dict] = []
invalid = 0
for path in files:
try:
record = json.loads(path.read_text())
except json.JSONDecodeError:
invalid += 1
continue
if is_valid(record):
valid.append(record)
else:
invalid += 1
counts: dict[str, int] = {}
for record in valid:
counts[record["category"]] = counts.get(record["category"], 0) + 1
print(f"files: {len(files)}")
print(f"valid: {len(valid)}")
print(f"invalid: {invalid}")
print(
"by_category: "
+ (", ".join(f"{name}={counts[name]}" for name in sorted(counts)) or "n/a")
)
if not valid:
print("amount_min: n/a")
print("amount_max: n/a")
print("amount_mean: n/a")
return
amounts = [record["amount"] for record in valid]
print(f"amount_min: {min(amounts)}")
print(f"amount_max: {max(amounts)}")
print(f"amount_mean: {sum(amounts) / len(amounts)}")
O(n) time and O(n) space, where n is the number of JSON files: one pass, and the valid records are kept so the mean can be computed. A running count, sum, min and max would drop the extra space to O(c) for c categories. statistics.mean is the same algorithm with an import.
Log it in the drill log (A5): date, problem, language, mode (NO AI), minutes, solved, the bool/int trap if it caught you.
Job search (20 min)
Yesterday’s tracker has 10 companies and an empty Applied column. Today, draft the FDE resume summary and three bullets from the claim ledger. The Applied AI variant is still owed this week; do not start it until these three bullets are done.
- Open the ledger. Use only rows marked Keep. A Rewrite row may contribute a bullet after you have applied the rewrite. A Remove row contributes nothing.
- Write a two- or three-line summary aimed at a forward-deployed role: who you build for, what you ship, what you can defend. Any number in it must be a number that survived the audit, with the same wording as the ledger.
- Write three bullets. Each one is an outcome from a Keep row: what changed, what you personally did, and the measure only if the ledger has the measure. SWIFTCLAIM does not appear. It is not evidence yet.
- Read the summary aloud. If a sentence would fail the question “show me how you measured that”, delete the number or delete the sentence.
Summary: “Engineer who <outcome from a Keep row>. <What you built, at the scope you built it>.” Bullet: “<Changed X for Y> by <your part>, measured as <method on the ledger>.” If the row has no method, the bullet has no number.
Done means
- You can explain aloud, in under three minutes, why a longer prompt and a longer answer each add cost, and why
usage.output_tokenscan exceed the length of the text you see. - You can say what
end_turnguarantees (the model stopped) and what it does not guarantee (that the content is right). - One real Claude call is logged with stop reason, both token counts and a cost you can recompute. Tests pass. The key and the log are outside git.
- The Python drill is solved without AI and logged in A5.
- The FDE resume summary and three bullets exist, and every number in them is a Keep-row number.
Resources
- Working with the Messages API: the request shape this client implements.
- Context windows: what counts, and the two ways a request runs out of room.
- Pricing: the rates you multiply. Re-read it on the day you log a cost.
- Andrej Karpathy, “Intro to Large Language Models”: next-token prediction, from the week’s resource list.