Chapter 7 · AI-Curious Developer

Request → parse → validate (handling an LLM response)

Calling a model is always the same three-beat shape: request → parse → validate. You send the messages, you dig the part you want out of the response, and you *check it before trusting it*.

The response isn't a bare string — it's a structured dict, and the text you want is buried a few layers deep. A typical shape:

  • response["content"] — a list of content blocks
  • response["content"][0] — the first block, a dict
  • response["content"][0]["text"] — the actual text

The key mindset: a model response is untrusted data. Treat it like a parsed web form or an API payload, not like a value your own code produced. The shape can be wrong, the list can be empty, the type can be something you didn't expect. So you *validate* — check the keys and types exist — and handle the bad case deliberately (return a default, or raise a clear error) rather than letting a raw KeyError/IndexError blow up later.

In the examples below the response is a hardcoded dict so it runs offline; in production that exact dict comes back from the model.

Syntax

# A response is a nested dict — parse, then validate before trusting it:
response = {
    "content": [{"type": "text", "text": "..."}],
    "stop_reason": "end_turn",
}

text = response["content"][0]["text"]   # parse

if "content" not in response or not response["content"]:
    raise ValueError("Invalid LLM response")   # validate

Worked examples

Parse: dig the text out of the nested response

response = {
    "id": "msg_001",
    "content": [{"type": "text", "text": "The answer is 4."}],
    "stop_reason": "end_turn",
}

def extract_text(response):
    return response["content"][0]["text"]

print(extract_text(response))
# Output:
# The answer is 4.

Validate: check the shape before trusting it

def is_valid_response(response):
    if "content" not in response:
        return False
    if not response["content"]:          # empty list
        return False
    if response["content"][0].get("type") != "text":
        return False
    return True

print(is_valid_response({"content": [{"type": "text", "text": "ok"}]}))
print(is_valid_response({}))
print(is_valid_response({"content": []}))
# Output:
# True
# False
# False

The full request→parse→validate flow

def call_and_extract(response):
    # (in production: response = call_model(messages))
    if not is_valid_response(response):
        raise ValueError("Invalid LLM response")
    return extract_text(response)

print(call_and_extract({"content": [{"type": "text", "text": "Hi"}]}))
try:
    call_and_extract({"content": []})
except ValueError as e:
    print("raised:", e)
# Output:
# Hi
# raised: Invalid LLM response

Common mistakes & gotchas

Reaching in without checking crashes

response["content"][0]["text"] raises KeyError (missing key), IndexError (empty list) or TypeError (wrong shape) the moment the response isn't what you assumed. Validate first, or use .get() with defaults — don't assume the happy path.

Treating the response as trusted

Model output is external input. Even when it looks like text you asked for, validate it as you would a web form: the model can return an error block, a refusal, a tool call, or an unexpected type. "It worked in my one test" is not validation.

Swallowing the failure silently

Catching a bad response and returning None (or "") without a trace hides the problem until something downstream breaks confusingly. Decide deliberately: raise a clear error, or return a documented default — but make the bad case visible.

Why it matters

Request→parse→validate is the spine of every integration you'll ever write — models, payment APIs, third-party services. The data crosses a boundary you don't control, so you parse defensively and validate before you trust. Get this reflex right and whole classes of production crash simply never happen.