My first useful call against our documentation assistant returned a two sentence answer and this usage line:
Question asked was forty words long. Everything else in that 1,163 was instructions I had written once and forgotten about, plus three paragraphs of product documentation I had pasted in to give the model something to work from. Twelve times more input than the user typed, on every single request, forever. That ratio is the first thing worth internalising about calling a model from code: you are not billed for the question, you are billed for the entire conversation you reconstruct and resend each time.
Key takeaways
A model call is stateless. Every turn resends the whole message list, so context length and cost grow together on every request.
Temperature is the parameter everyone teaches first and it is not accepted at all by current reasoning models. Sending it returns a 400.
Two fields decide whether a response is trustworthy: the finish status and the token usage. Most first implementations read neither.
Streaming does not make a response faster. On our assistant it moved time to first visible text from 5.1 seconds to 410 ms while total time went up slightly.
Wrap the provider call in one function on day one. It costs twenty minutes and it is the difference between a config change and a refactor when you switch.
Shape of a model request
Every major provider exposes roughly the same thing: an HTTPS endpoint that accepts a list of messages and returns one more message. Each message carries a role and some content. Roles differ slightly by vendor but three concepts recur. Developer or system content sets standing behaviour and is not part of the conversation. User content is what the person typed. Assistant content is what the model said on previous turns, which you resend so it can see its own history.
Statelessness is the part that surprises backend developers, because it inverts a habit. HTTP sessions taught us that the server remembers and the client sends a token. Here it is reversed: your process is the memory. If a user asks a follow up question, nothing on the provider side knows what came before unless you paste it back in. Some APIs now offer server side conversation storage as an option, but the billing model underneath is unchanged, and the input still gets re tokenised on every call. Context length is a hard ceiling on how much history you can carry, which context window explained covers properly.
First call from Python
Our running project through this series is an internal documentation and support assistant for a mid sized SaaS company. Right now it does not exist. By the end of this part it will be a single function that answers a question from a chunk of documentation you pasted in by hand, which is a deliberately poor design that Parts 8 to 13 replace with real retrieval. Getting the call correct first means the retrieval work later has something reliable to sit on.
Install the SDK and pin it. Provider SDKs change signatures more often than most libraries you depend on, and an unpinned install will break a working script three weeks later with no code change on your side. Set your key as an environment variable and never put it in the file. Every code block below reads from the environment.
Three things in that snippet deserve attention. Separating instructions from input keeps standing behaviour out of the user turn, which matters later when you start logging and replaying user inputs on their own. Reading the key from the environment means the file is safe to commit. Printing status and usage rather than only output_text is the habit that saves you in the section after next.
On the Anthropic side the same call is client.messages.create with a system argument instead of instructions, messages as an explicit list of role and content dictionaries, and max_tokens rather than max_output_tokens. Text arrives at resp.content[0].text and the finish reason at resp.stop_reason. Concepts map one to one; only the field names move.
Parameters that matter, and parameters that do not
Here is where a lot of received wisdom has gone stale. Nearly every introduction to LLM APIs opens with temperature, presents it as the main dial, and moves on. On current reasoning models that parameter is not merely discouraged, it is rejected outright. Sampling controls were removed because the model no longer generates a single pass answer that you can usefully perturb; it produces internal reasoning first, and the knob that governs quality and cost is how much of that reasoning you are paying for.
Fix is to drop the parameter, not to set it differently. Reach for reasoning effort and verbosity instead. Effort governs how much internal thinking the model does before answering, which drives both latency and the output token bill. Verbosity governs how long the visible answer is, independently of how hard the model thought. Conflating those two is a common and expensive mistake: teams turn effort up because answers felt thin, when what they wanted was a longer final answer at the same effort.
Below is the reference I keep pinned. It is worth reading once properly rather than discovering each row through a production incident.
| Parameter | What it controls | Where to start | What breaks if you get it wrong |
|---|---|---|---|
| max_output_tokens | Ceiling on generated tokens, including invisible reasoning tokens | 4x your longest expected answer | Silent truncation mid sentence. Worst failure in this table. |
| reasoning effort | How much internal thinking happens before the answer | Lowest setting that passes your eval set | Latency and output token spend rise with no accuracy gain |
| verbosity | Length of the visible answer only | Low for structured output, medium for prose | Answers feel padded, or get clipped by downstream UI |
| temperature | Randomness of token sampling on non reasoning models | Omit entirely unless you know the model accepts it | 400 on reasoning models. Unreproducible output elsewhere. |
| top_p | Nucleus sampling cutoff, an alternative to temperature | Leave alone. Never set alongside temperature. | Interacts with temperature in ways nobody can debug |
| instructions or system | Standing behaviour, separate from the user turn | Always use it. Do not prepend rules to user text. | Rules become user input, and injectable. See Part 24. |
| timeout | Client side deadline on the HTTP request | Set explicitly. Defaults are long. | Worker threads pile up behind one slow call |
Temperature, measured rather than assumed
Plenty of models still accept temperature, so it is worth knowing what it actually buys. Second piece of received wisdom to retire: setting temperature to zero does not give you a deterministic system. It makes sampling greedy, which is not the same thing. Batching on the serving side, floating point non associativity across different hardware, and silent model updates all move output under you. I measured this rather than argue about it, asking one fixed question twenty times at four settings and counting distinct answers.
Practical consequence: do not build a cache key, a test assertion or a diff on the assumption that identical input yields identical output. Assert on structure and on meaning, which is what Parts 20 through 22 build properly. For the underlying reason variation and confident wrongness travel together, why AI hallucinates and what temperature really does is the GenAI series companion to this section.
Truncation and other silent failures
Here is the mistake that cost me the most embarrassment on this project. Early version of the assistant summarised support tickets, and I set max_output_tokens to 512 because summaries are short and I was being frugal. Everything looked fine in testing. Three weeks later a customer success manager forwarded a summary that ended mid clause, at the words "the customer confirmed that the refund was", and asked what the rest said. There was no rest. Roughly four percent of summaries had been truncated for three weeks, because longer tickets produced longer summaries and I had only ever tested on short ones.
Nothing raised. No exception, no warning, no non zero exit. Response came back with a status of incomplete and a reason of max_output_tokens, and my code read output_text and ignored everything else. That is the default shape of a first implementation and it is why the very first code block in this part printed status. Reasoning models make this sharper still, because invisible reasoning tokens count against the same ceiling, so a request can burn its entire budget thinking and return an empty string.
On Anthropic the same check reads resp.stop_reason and compares against max_tokens rather than inspecting an incomplete_details object. Same defect, same fix, different field name. Set the ceiling generously and treat it as a circuit breaker rather than a budget, because output tokens are billed on what is generated, not on what you allowed.
Production gotcha
A raised max_output_tokens ceiling costs nothing until it is used, but a low one corrupts output invisibly. Set it to about four times your longest expected answer and enforce length through verbosity and your prompt instead. Then log status and usage on every call from the very first commit, because reconstructing a truncation rate after the fact is impossible if you never recorded the field.
Streaming and perceived latency
Generation is sequential, so a long answer takes a long time. Streaming does not change that, it changes when the user sees the first word. On our assistant a typical grounded answer took 5.1 seconds end to end. Streamed, first visible text appeared at 410 milliseconds, and total completion drifted marginally later at around 5.3 seconds because of per event overhead. Users described the streamed version as much faster. Same work, thirteen times better first impression.
Cost of streaming is not latency, it is that you lose the ability to inspect a complete response before showing it. Anything you plan to validate, filter or parse must not be streamed to the user directly, because you cannot un show a sentence you have already rendered. Structured output in Part 4 and guardrails in Part 23 both need the whole response in hand. My rule: stream prose answers, never stream anything that a schema or a filter has to approve first.
Keeping the call site provider neutral
Every field name that differs between providers is a place your application can grow a dependency it did not intend. Instructions against system, max_output_tokens against max_tokens, status against stop_reason, output_text against content[0].text. None of those differences are interesting, and all of them will end up scattered through your codebase if you call the SDK directly from twenty places. Twenty minutes of work now prevents a genuinely tedious refactor later, and Part 29 builds on exactly this seam.
Resist the urge to make this abstraction clever. It should normalise names and nothing else. Every team I have seen grow a general purpose LLM abstraction layer ended up with something that supported the intersection of provider features rather than the union, and quietly blocked adoption of whatever shipped next. Frameworks such as LangChain and LlamaIndex offer a richer version of this seam and are a reasonable choice when you want their retrieval and agent pieces too, but for a first project a thirty line dataclass is easier to debug and hides nothing from you.
Log status and usage before you write another prompt
If you take one action from this part, make it this: every call site records the finish status and both token counts alongside the response, from the first commit. Truncation rate, cost per request and the effect of any prompt change all become measurable the moment those three fields are in your logs, and all three become archaeology if they are not. Costs nothing. Nobody does it until it has already bitten them.
Second recommendation, narrower: pin your SDK version, drop temperature from any call to a reasoning model, and put reasoning effort at the lowest setting you can defend rather than the highest you can afford. Our assistant now has a working call. It is expensive, it knows only what you paste into it, and it has no idea whether its answers are any good. Part 3 tackles the prompt itself, and specifically the patterns that hold up when real users start typing things you did not anticipate.
References
- OpenAI, Migrate to the Responses API, for the recommendation that new projects start on Responses rather than Chat Completions, and for the reasoning model behaviour differences between the two surfaces.
- OpenAI API reference, Responses, for the definitions of input, instructions, max_output_tokens and temperature used in the parameter table.
- openai-python releases, for version 2.46.0, released 17 July 2026, which is the version every OpenAI code block here was tested against.
- Claude Platform docs, Python SDK, for messages.create, the system argument, max_tokens and stop_reason as used in the provider neutral wrapper.
- OpenAI Cookbook, GPT-5 new params and tools, for reasoning effort and verbosity as the replacements for sampling controls on reasoning models.


DrJha