Your first API call
The request shape every LLM API shares, working examples for the major providers, and the four things that break in production.
Data checked 2026-09-06The shape every provider shares
Text-generation APIs converged on the same request shape years ago. You send a list of messages, each with a role and content, plus the model name and a cap on how many tokens the model may generate. You get back the generated message, a reason it stopped, and a token count for billing.
POST /v1/messages (or /v1/chat/completions, or :generateContent)
Authorization or x-api-key your key
{
"model": "<model id from /models/>",
"messages": [{"role": "user", "content": "..."}],
"max_tokens": 4096
}
Learn that shape once and every provider in the model index is a variation on it. The variations that actually matter are the authentication header, the name of the token cap, and how streaming and tool calls are encoded.
| Provider | Endpoint | Auth header | Token cap field |
|---|---|---|---|
| Anthropic | /v1/messages | x-api-key + anthropic-version | max_tokens |
| OpenAI | /v1/chat/completions or /v1/responses | Authorization: Bearer | max_completion_tokens |
:generateContent | x-goog-api-key | maxOutputTokens | |
| OpenAI-compatible hosts | /v1/chat/completions | Authorization: Bearer | max_tokens |
That last row covers most of the inference platforms and every gateway. They deliberately imitate OpenAI's request format, so one client library reaches dozens of open-weight models by changing a base URL. It is the closest thing this industry has to a portability standard, and it is a convention rather than a spec — expect the edges (tool calling, structured output, caching) to differ even where the core matches.
Claude
Install the official SDK — pip install anthropic — and set ANTHROPIC_API_KEY in your environment.
from anthropic import Anthropic
client = Anthropic()
response = client.messages.create(
model="claude-opus-5",
max_tokens=16000,
thinking={"type": "adaptive"},
messages=[{"role": "user", "content": "Summarise this changelog in three bullets."}],
)
print(response.content[0].text)
print(response.usage.input_tokens, response.usage.output_tokens)
thinking={"type": "adaptive"} lets the model decide how much to reason before answering; leave it on for anything non-trivial. Control the depth with output_config={"effort": "low"|"medium"|"high"|"xhigh"|"max"} rather than a fixed token budget. Anything that might produce a long response should stream instead, which avoids HTTP timeouts:
with client.messages.stream(
model="claude-opus-5",
max_tokens=64000,
messages=[{"role": "user", "content": "Write the migration plan."}],
) as stream:
final = stream.get_final_message()
TypeScript is the same shape with @anthropic-ai/sdk:
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const response = await client.messages.create({
model: "claude-opus-5",
max_tokens: 16000,
thinking: { type: "adaptive" },
messages: [{ role: "user", content: "Summarise this changelog in three bullets." }],
});
OpenAI-compatible providers
Because so many hosts speak the OpenAI format, one client covers a large share of the providers listed here. Change base_url and the model name; the rest holds.
from openai import OpenAI
client = OpenAI(base_url="https://api.example-host.com/v1", api_key=KEY)
response = client.chat.completions.create(
model="<model id>",
messages=[{"role": "user", "content": "Summarise this changelog in three bullets."}],
max_tokens=4096,
)
print(response.choices[0].message.content)
A compatibility layer is not the real API. Providers that offer an OpenAI-shaped endpoint alongside their own usually treat it as a migration and evaluation aid — prompt caching, strict tool schemas and structured outputs are commonly ignored or unsupported there. Build production integrations against the provider's native SDK and keep the compatible endpoint for portability testing.
Multi-turn conversations cost more than you think
There is no server-side conversation. Each request is stateless, so continuing a conversation means resending the entire history every time. Append the model's reply to your message list and send the whole thing back:
messages = [{"role": "user", "content": "What changed in the API?"}]
reply = client.messages.create(model="claude-opus-5", max_tokens=16000, messages=messages)
messages.append({"role": "assistant", "content": reply.content})
messages.append({"role": "user", "content": "Which of those are breaking?"})
Turn ten bills for all nine previous turns plus the new one. A long conversation's cost grows with the square of its length, which is the single most common surprise on a first invoice. See understanding token pricing for the arithmetic, and the cost calculator to put your own volumes against real rates.
The four things that break in production
Rate limits. Every provider enforces requests and tokens per minute, usually tiered by spend. A 429 is normal traffic-shaping, not an error in your code. Back off exponentially with jitter; the official SDKs retry automatically, and you should not add a second retry layer on top without checking.
Truncated output. If stop_reason is max_tokens, the model was cut off mid-sentence and you got a partial answer that may still parse as valid. Always check the stop reason before trusting the content. Set max_tokens generously — it is a ceiling, not a reservation, and you are billed for what is generated, not what you allow.
Timeouts on long generations. A large max_tokens on a non-streaming request will eventually exceed an HTTP timeout somewhere in the chain. Stream anything that might run long.
Silent cost drift. Reasoning models emit thinking tokens that you are billed for but may never see in the response body. A model that looks comparable on the rate card can cost several times more per completed task. Measure usage on real traffic rather than estimating from published prices.
What people get wrong
Hardcoding a model ID forever. Model IDs are deprecated and retired on published schedules. Read the model name from configuration, not from a string literal buried in a prompt module, and check the status column in the model index before you pin.
Putting the API key in the browser. A key in client-side JavaScript is a public key. Every request must go through a server you control, which is also where you add rate limiting and abuse controls.
Retrying non-retryable errors. A 400 means your request is malformed; retrying it produces the same 400 and burns quota. Retry 408, 409, 429 and 5xx. Do not retry 400, 401, 403 or 404.
Assuming temperature 0 is deterministic. It reduces variation but does not guarantee identical output across runs, and several current reasoning models reject sampling parameters entirely. Write tests that assert on properties of the output, not on exact strings.
Skipping token counting. Do not estimate tokens by dividing characters by four, and do not use another vendor's tokenizer. Providers expose a token-counting endpoint; use it when you need an accurate number.
Once a single call works, the next decisions are which model to standardise on (how to choose a model) and how to stop the bill growing (cutting API costs).