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 blocksresponse["content"][0]— the first block, a dictresponse["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") # validateWorked 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
# FalseThe 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 responseCommon 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.