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).

← Back

🔒 Answer the question correctly to move on to the next lesson.

Your first working client: code and response handling — AI for developers: APIs and agents — Skilvy