Streaming and state
Send a real request to a model from the provider of your choice, then make the surrounding program trustworthy: constrain the output, validate it, handle streaming and failures, and keep cost bounded.
THE IDEA
Text that arrives in pieces
A long answer can take many seconds. With streaming the provider sends the reply as it is produced, as a series of small deltas (a few characters or tokens each), so a user sees words appear early. The usage numbers and the stop reason arrive at the end, with the final message.
That creates a state your program did not have before: the answer exists, but only partly. Between the first delta and the end the text is a fragment. You need to decide what you hold, what you show, and when you may parse and act.
The answer is an object, so no prefix is valid until the closing brace arrives.
{"city": "ParJSON.parse of what you hold: throws, this is a fragment
State: streaming. Show the text if you like, but do not act on it yet.
Fragments that parse are the dangerous kind
A prefix of a JSON object is invalid until the closing brace, which is why a naive parser fails loudly. Other outputs fail quietly: choose the number streams above. The text 210 is valid JSON, it is just the wrong number, and if the reply was cut off by the output limit nothing in the text tells you.
Predict first. Halfway through a stream you call JSON.parse on the text so far, and it succeeds. Can you act on the value?
Render deltas as they come, keep the accumulated string, and parse once after a natural stop.
Run side effects (saving a record, calling a tool) on a partial parse, then be surprised when the user hits Cancel.
Chapter context
A model API turns input messages into generated output, but the program around it stays responsible for validation, retries and side effects. Every major provider (Anthropic, OpenAI, Google, Mistral, Groq, OpenRouter and local OpenAI-compatible servers) exposes the same few ideas under different field names: a model id, a list of messages with optional system instructions, an output-token limit, and a response carrying the text, a reason it stopped, and token usage. The labs in this chapter run against whichever provider you choose with your own key, so you see real behavior rather than a mock.
TRY IT YOURSELF
Part 1. Stream a real answer
Bring your own key from Anthropic, OpenAI, Google, Mistral, Groq, OpenRouter, xAI, DeepSeek, or point the Custom option at a local OpenAI-compatible server. The key stays in your browser and goes straight to the provider. With no key, the lab shows a clearly labelled sample response so you can still follow along.
Run it to the end first. Cancel needs a real key, because the sample replay cannot be interrupted.
TRY IT YOURSELF
Watch a real answer arrive
Stream a JSON answer delta by delta. After every delta the lab tries JSON.parse on what you hold so far and counts the failures.
Let the stream finish and see that several snapshots along the way were not valid JSON while the completed output is. Then run it again and cancel halfway to see what you are left holding.
Prompt: Return only a JSON object with the keys city, country and population for Paris. No prose, no markdown.
This streaming request as code
import anthropic
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY
with client.messages.stream(
model="claude-haiku-4-5", # Model
max_tokens=120, # max output tokens
messages=[{"role": "user", "content": "Return only a JSON object with the keys city, country and population for Paris. No prose, no markdown."}], # User message
) as stream:
for text in stream.text_stream: # one small delta at a time
print(text, end="", flush=True)
final = stream.get_final_message() # complete only now
print(final.stop_reason, final.usage.input_tokens, final.usage.output_tokens)Update the controls above and this code changes with them. Run it yourself with the key in an environment variable.…
State: idle · 0 deltas · 0 snapshots that failed JSON.parse
Part 2. Code: a stream consumer that never acts on a fragment
eventsyields("delta", text)tuples and finishes with("stop", reason). Writeconsume(events, cancel_after=None)returning{"state", "text", "value"}.- Accumulate the deltas into
text. After the stream stops with a natural reason, parsetextas JSON:stateisdonewithvalueset to the parsed result, orinvalid(value None) if it does not parse. - A stop reason of
max_tokensorlengthmeanstruncated, even if the text happens to parse (a cut-off number does).cancel_after=kmeans stop reading right after the k-th delta and returncancelledwithout pulling any further event. - If the stream raises
ConnectionError, or ends without any stop event, returnerror. In every state exceptdone,valueisNone: a fragment is never a result.
Both parts must be solved to finish this lesson.
Stuck, or something not working? You can come back to it any time.
CHECK YOUR UNDERSTANDING
Halfway through a stream, can you pass the text received so far to JSON.parse and act on it?
Chapter reference: math, worked example & code
This section belongs to the whole chapter, so it is the same on every lesson in it and may cover a different lesson's topic. The code for the experiment you just ran is inside the experiment itself.
Math and notation
Four small rules recur through the chapter.
- Complete or cut off (F8.1): treat the reply as an answer only when the provider's stop reason means a natural stop (
end_turn,stop,STOP), not a limit (max_tokens,length,MAX_TOKENS). - Validation order (F8.2-F8.4): parse, then check fields, then check domain rules. Passing one stage says nothing about the next.
- Backoff (F8.5): wait before retry , where is the base delay and the jitter factor is a random number in (or without jitter). Retry only temporary errors, and stop after a fixed number of retries.
- Cost (F8.6): for prices and per million input and output tokens, and token counts and :
With an output cap , the worst-case output side for calls is . The prices below are invented arithmetic inputs, not a product recommendation; use the provider's current price list for real budgeting.
Worked example
One small helper per idea, all checked offline. A reply stopped with end_turn is usable but one stopped with max_tokens is not. The text {"sentiment": "neutral", "confidence": 0.7} passes all three validation stages, while a confidence of 7 or a reply with chatty text around the JSON is rejected. A 429 is retried but a 401 is not, and the delays for retries 0, 1 and 2 at a 0.5 s base are 0.5, 1.0 and 2.0 seconds. At illustrative prices of 6 per million tokens, a call with 1,000 input and 500 output tokens costs (1000×2 + 500×6)/1,000,000 = $0.005.
Reference code
The following example uses the Python standard library and does not call a remote service. It is the same logic the labs let you try against a real model.
import json
from math import isclose
def usable(stop_reason): # F8.1: a limit means the text may be cut off
return stop_reason in {"end_turn", "stop", "STOP"}
def validate(text): # F8.3: syntax, then fields, then rules
data = json.loads(text) # raises ValueError on bad syntax
if data.get("sentiment") not in {"positive", "negative", "neutral"}:
raise ValueError("bad sentiment")
if not 0 <= data.get("confidence", -1) <= 1:
raise ValueError("confidence out of range")
return data
def should_retry(status, attempt, max_retries=3): # F8.5: temporary errors only, bounded
return status in {429, 529} and attempt < max_retries
def delay(attempt, base=0.5, jitter=1.0): # F8.5: exponential backoff
return base * 2 ** attempt * jitter
def cost(n_in, n_out, p_in=2, p_out=6): # F8.6: illustrative prices
return (n_in * p_in + n_out * p_out) / 1_000_000
assert usable("end_turn") and not usable("max_tokens") and not usable("length")
assert validate('{"sentiment": "neutral", "confidence": 0.7}')["confidence"] == 0.7
for bad in ['{"sentiment": "neutral", "confidence": 7}', 'Sure! {"sentiment": "neutral"}']:
try:
validate(bad)
except ValueError:
pass
else:
raise AssertionError("accepted bad output")
assert should_retry(429, 0) and not should_retry(401, 0) and not should_retry(429, 3)
assert [delay(a) for a in range(3)] == [0.5, 1.0, 2.0]
assert isclose(cost(1000, 500), 0.005)
assert isclose(cost(1000, 500) * 10_000, 50)
Python examples are provided for study and local execution; this page does not execute Python.
Common mistakes
JSON output can still be false or out of range. A model call is not required for every rule-based task. Never place a reusable private API credential in a public browser bundle: the labs here use a key you paste yourself, kept in your browser and sent only to the provider, and a shipped product would call the provider from a server instead. Reasoning models can spend part of the output limit on hidden reasoning, so a tiny limit may return empty text.