promptdojo_

Wire the real model — swap fake_llm for the Anthropic SDK shape — step 3 of 9

The real SDK's response, line by line

The Anthropic Python SDK returns an anthropic.types.Message object. Pyodide can't hit the wire to make a real call, but the shape is what matters — and the mock above matches the real object exactly. Same attribute names, same types.

Five things to lock in:

1. response.stop_reason is a string

"end_turn", "tool_use", "max_tokens", "stop_sequence", "pause_turn", "refusal". Same five values you learned in chapter 16. The two that drive the loop are still end_turn and tool_use; the others are exception paths.

2. response.content is a LIST of blocks

Even when the model only said one sentence, content is [TextBlock("...")]. Always indexable, always iterable. Beginners write response.content and try to print it as if it's a string — gets you <TextBlock object at 0x7f...> and a sad face. Iterate the list.

3. Each block has a .type field — branch on it FIRST

for block in response.content:
    if block.type == "text":
        print(block.text)            # text-only attribute
    elif block.type == "tool_use":
        print(block.name, block.input, block.id)  # tool_use attributes

Reading block.text on a tool_use block raises AttributeError. Reading block.name on a text block raises AttributeError. The type field is the discriminator. Always.

A subtle thing: when extended thinking is enabled, you may also see thinking blocks. Add them to your branching or filter them out before processing — they're not for the user.

4. response.usage carries the bill

response.usage.input_tokens     # what you sent
response.usage.output_tokens    # what you got back
response.usage.cache_read_input_tokens     # cache hits (huge cost win)
response.usage.cache_creation_input_tokens # cache writes (1.25× input cost)

Every turn adds these numbers. A 50-turn agent that doesn't read its own usage field is an agent whose owner finds out about the bill at the end of the month.

5. The dict-style mock from lesson 1 is almost right

The fake_llm in lesson 1 returned {"stop_reason": "end_turn", "content": [{"type": "text", "text": "..."}]}. That's a dict shape. The real SDK returns objects with attribute access. Easy fix in step 8 — wrap the dicts in classes so .type and .text work the same as the real SDK. Then your loop runs on either.

Why bother with the mock when you could just use the real SDK?

Three reasons:

  1. Pyodide can't make HTTPS calls. No real SDK in the browser.
  2. Tests should be deterministic. Real models return slightly different text every call — your eval suite goes flaky.
  3. You want a no-key escape hatch. Demo mode, offline dev, CI without secrets — all need a mock that matches the real shape.

Step 8 builds that mock in 30 lines. Then your agent loop accepts either a MockMessage or a real anthropic.types.Message and behaves identically.

read, then continue.