Your first working client: code and response handling
AI for developers: APIs and agents · Lesson 2 / 20
Your first working client
Let's write not a "hello world" but a minimal client you could actually ship: with a timeout, error checking and spend logging.
Node.js
const MODEL = "gpt-4o-mini";
export async function ask(messages, { maxTokens = 500, temperature = 0.2 } = {}) {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), 30_000); // always set a timeout
try {
const r = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
signal: ctrl.signal,
headers: {
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ model: MODEL, messages, temperature, max_tokens: maxTokens }),
});
if (!r.ok) {
const text = await r.text();
throw new Error(`LLM ${r.status}: ${text.slice(0, 300)}`);
}
const data = await r.json();
const choice = data.choices?.[0];
if (choice?.finish_reason === "length") {
console.warn("Answer truncated by max_tokens — raise the cap or shorten the input");
}
console.info("usage", data.usage); // log the spend
return choice?.message?.content ?? "";
} finally {
clearTimeout(timer);
}
}
Python
import os, httpx
MODEL = "gpt-4o-mini"
def ask(messages, max_tokens=500, temperature=0.2):
r = httpx.post(
"https://api.openai.com/v1/chat/completions",
headers={"Authorization": f"Bearer {os.environ['OPENAI_API_KEY']}"},
json={"model": MODEL, "messages": messages,
"temperature": temperature, "max_tokens": max_tokens},
timeout=30.0,
)
r.raise_for_status()
data = r.json()
choice = data["choices"][0]
if choice["finish_reason"] == "length":
print("WARN: answer truncated")
print("usage:", data["usage"])
return choice["message"]["content"]
What matters here
- The timeout. Without one a request can hang for minutes holding a connection. 30 seconds is a sane start for short answers.
- Checking
r.ok. The API returns meaningful errors in the body — don't swallow them, log the first 300 characters. - Logging usage. One line that will later save you from reverse-engineering an invoice.
- Model in a constant. Don't scatter the model string through your code: you'll be changing it often.
Where the key lives
Server-side environment variables only. Not in the repo, not in the client bundle, not in NEXT_PUBLIC_*. If a key ever lands in git, revoke it and issue a new one — history is forever.
Insight. Wrap the model call in a single function from day one. Retries, caching, metrics and provider switching will all land here later — in one place.
Common mistake. Calling the API straight from a React component. The key leaks to the browser; a backend proxy is mandatory.
Pro tip. Add a
requestId parameter and thread it into your logs. Debugging production gets dramatically easier.Cheat sheet
- A timeout is mandatory.
- Check the status and log the error body.
- Log usage from the very first call.
- Keys stay on the server.
1. What must you add to an HTTP call to a model?
2. Where may the API key live?
3. Why wrap the call in a single function?
Task — checked by AI
Write a minimal LLM API client in your language: with a timeout, status checking, finish_reason handling and usage logging. Make one real request. Paste the client code and the output (including usage).