Home

Chapter 1 · Lesson 0002

Your first LLM API call — how OpenAI works

30–40 min read · Hands-on coding

Motivation

The most common task in AI engineering is deceptively simple: send text in, get text back. A customer sends a support message — your code sends it to an LLM, gets a draft reply, and routes it to the right team. A user uploads a contract — your code sends the text to an LLM, gets a structured summary, and stores it in a database.

Everything else in this curriculum — agents, RAG, production deployments — is built on top of that one primitive: call the LLM, read the response. This lesson teaches you exactly how that works, from the first line of code to handling errors gracefully.

Prerequisites

  • Lesson 1: uv project created, openai and python-dotenv installed, .env file set up
  • API access — either an OpenAI API key (requires a small credit top-up) or a free OpenRouter key — see the OpenRouter section in this lesson if you prefer to start without spending anything

What is an LLM?

A Large Language Model — LLM for short — is a type of AI model that takes text as input and produces text as output. When you type a question into ChatGPT and get an answer, you are using an LLM. When GitHub Copilot suggests the next line of code, that is an LLM. When Google translates a document, that is an LLM.

Under the hood, an LLM is a massive mathematical function — billions of numbers (called parameters) arranged so that given some input text, the model can predict what text should come next. It does this one small piece at a time. Those small pieces are called tokens.

A token is the unit an LLM uses to measure and process text. A token is roughly four characters — about three-quarters of an average English word. (Words average five or more characters plus spaces, so one token covers slightly less than one word — four characters per token × 1,000,000 tokens = 4,000,000 characters; at five-plus characters per word, that is roughly 750,000 words.) The word “hello” is one token. The word “unfortunately” is four tokens. A full paragraph might be 80–100 tokens. Tokens matter because LLM providers charge you per token — more text means more cost.

As an AI engineer, you will never train or modify an LLM. These models take months and millions of dollars to train, and that work is done by companies like OpenAI and Anthropic. Your job is to call them via an API — the same way a web developer calls a weather API, not writes the meteorological simulation.

What is an API?

An API (Application Programming Interface) is a way for one piece of software to talk to another over the internet. When your app needs today’s weather, it sends a request to a weather service’s API and receives a structured response. Your code did not compute the weather — it asked a service that already knows it.

The OpenAI API works the same way. Your Python code sends a request to OpenAI’s servers. Those servers run the LLM, generate a response, and send it back to your code. The whole round trip takes one to five seconds for most requests.

OpenAI’s API is a standard web API — it works over HTTP, the same protocol your browser uses to load websites. The Python openai package is a library that handles all the HTTP details for you: it formats the request, sends it, waits for the response, and returns a clean Python object.

Free access with OpenRouter

Getting started requires API access, and every OpenAI account must be topped up with credit before the API works. For learning purposes, OpenRouter removes that barrier entirely.

OpenRouter is a service that routes your requests to hundreds of different LLMs — and it gives you two free options:

  1. Free models — a curated set of capable models (Llama, Qwen, Mistral variants and more) that carry a :free suffix and are permanently free with no daily cap.
  2. 50 free daily calls to any model — including paid ones like GPT-4o and Claude, enough to complete every coding task in this curriculum.

To set it up: sign up at openrouter.ai, go to Models, click Quick Start, and copy the code snippet. The pattern looks like this:

from openai import OpenAI
import os
from dotenv import load_dotenv

load_dotenv()

client = OpenAI(
    api_key=os.getenv("OPENROUTER_API_KEY"),
    base_url="https://openrouter.ai/api/v1",
)

The base_url parameter is what makes this work. Instead of sending requests to OpenAI’s servers, the openai library sends them to OpenRouter’s servers — which speak the exact same API language and forward the call to whichever model you specify. Everything else you learn in this lesson — messages, model, reading responses, handling errors — is identical.

Add your OpenRouter key to .env:

OPENROUTER_API_KEY=sk-or-v1-...
Model names on OpenRouter use a provider prefix: openai/gpt-4o-mini, anthropic/claude-haiku-4-5, meta-llama/llama-3.3-70b-instruct:free. Browse openrouter.ai/models for the full list and look for the :free suffix to find permanently-free models.

The rest of this lesson shows the direct OpenAI setup. Both approaches are valid — if you have an OpenAI key use it directly; if you are just starting out and prefer not to add credit yet, use OpenRouter with the base_url swap above.

Authentication — proving who you are

Every request you make to the OpenAI API must include your API key. OpenAI’s servers check the key, identify your account, and charge any usage to you. Without the key, the request is rejected.

The openai library reads your API key when you create a client object. The pattern looks like this:

from openai import OpenAI
import os
from dotenv import load_dotenv

load_dotenv()

client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

The OpenAI() call creates a client — an object that knows your API key and knows how to send requests. You create it once at the top of your script and reuse it for every API call. The key comes from the environment (loaded by load_dotenv()), never from the source code itself.

Shortcut: If your API key is in the environment variable OPENAI_API_KEY, the openai library picks it up automatically — you can write client = OpenAI() with no argument. Explicit is clearer for learning, so we pass it explicitly here.

Models — which LLM are you calling?

OpenAI offers several different LLMs, and you specify which one you want on every API call. Each LLM is called a model. Different models have different capabilities, speeds, and costs.

The main models you will use in this curriculum:

Model Best for Cost
gpt-4o-mini Learning, prototyping, simple tasks Very cheap (~$0.15 per million tokens in)
gpt-4o Complex reasoning, production tasks Moderate (~$2.50 per million tokens in)

Start with gpt-4o-mini for all your experiments. It is fast, inexpensive, and capable enough for almost every task in this chapter. You can always upgrade to gpt-4o for a specific task that needs more reasoning power.

A million tokens is about 750,000 words — roughly ten full novels. At those prices, the coding tasks in this curriculum will cost you less than $1 total.

The chat completions endpoint

Every call to an LLM goes through the chat completions endpoint. “Endpoint” is the API term for “the specific URL and action you are calling” — think of it like a function name for the API. The chat completions endpoint is OpenAI’s main interface: you send a conversation, and the model replies.

A chat completions call requires two things: the model name and a list of messages. The messages list represents the conversation. Here is the simplest possible call:

response = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[
        {"role": "user", "content": "What is the capital of France?"}
    ]
)

Each message in the list is a dictionary with two keys:

For a single question with no history, the messages list contains just one entry with role "user".

Reading the response

The API call returns a Python object — a structured response with several fields. The text you want lives at a specific path inside it:

text = response.choices[0].message.content
print(text)

Why such a long path? Because the response is designed to support several responses at once (the choices list), and each choice contains a full message object (just like the ones you send). For now, there is always exactly one choice, at index [0].

The response also tells you how many tokens the call used — useful for tracking cost:

print(response.usage.prompt_tokens)      # tokens in your messages
print(response.usage.completion_tokens)  # tokens in the reply
print(response.usage.total_tokens)       # sum of both

Complete working example

Here is every piece together — a complete script that makes a real API call and prints the result:

import os
from dotenv import load_dotenv
from openai import OpenAI

load_dotenv()

client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

response = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[
        {"role": "user", "content": "Explain what an API is in two sentences."}
    ]
)

text = response.choices[0].message.content
print(text)
print(f"\nTokens used: {response.usage.total_tokens}")

Run it with:

uv run main.py

You should see the model’s explanation of APIs, followed by the token count. If you see that, your entire environment — key, package, network, billing — is working.

Handling errors

API calls fail. Networks drop. Keys expire. Rate limits get hit. A professional script handles these cases instead of crashing with a confusing stack trace.

The openai package raises specific exception types for different failure modes. The three you will encounter most often:

Exception What it means What to do
AuthenticationError API key is wrong or missing Check your .env file — copy the key fresh from the dashboard
RateLimitError Too many requests in too short a time Wait a moment and retry; add a small delay in automated scripts
OpenAIError Any other API error (catch-all) Log the error message and surface it to the user or a monitoring system

Wrap your API calls in a try/except block:

from openai import OpenAI, AuthenticationError, RateLimitError, OpenAIError

try:
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": "Hello!"}]
    )
    print(response.choices[0].message.content)

except AuthenticationError:
    print("Error: invalid API key. Check your .env file.")
except RateLimitError:
    print("Error: rate limit hit. Wait a moment and try again.")
except OpenAIError as e:
    print(f"OpenAI API error: {e}")

In production AI systems, errors go to a structured logging system rather than print() — but the pattern is the same. Catch the specific exceptions you can handle, catch the generic one as a fallback.

Anatomy of a complete, production-ready call

Here is what a well-written API call looks like when all the pieces come together. This is the pattern you will follow throughout the curriculum:

import os
from dotenv import load_dotenv
from openai import OpenAI, AuthenticationError, RateLimitError, OpenAIError

load_dotenv()

client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

def ask(question: str) -> str:
    """Send a single question to gpt-4o-mini and return the text reply."""
    try:
        response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": question}]
        )
        return response.choices[0].message.content

    except AuthenticationError:
        raise RuntimeError("Invalid API key — check your .env file.")
    except RateLimitError:
        raise RuntimeError("Rate limit hit — wait a moment and retry.")
    except OpenAIError as e:
        raise RuntimeError(f"OpenAI error: {e}") from e


if __name__ == "__main__":
    answer = ask("What is a token in the context of LLMs?")
    print(answer)

A few design choices worth noting:

Assignment

  1. Read: What Is an LLM? by Matt Pocock (aihero.dev) — about 10 minutes. Written by a practitioner, not a researcher. It explains what LLMs are in terms that map to your job as an engineer: what you give them, what you get back, and where their limits are. Read it after making your first API call so the vocabulary clicks.
  2. Do: Write a script that takes a question from the command line, sends it to gpt-4o-mini, and prints the answer plus the token count.
    1. In your ai-foundations project, edit main.py
    2. Use import sys and sys.argv[1] to read the question from the command line
    3. Call the API with that question and print the response text
    4. Below the response, print: Tokens used: N
    5. Add a try/except that catches AuthenticationError and prints a helpful message

    Expected output: uv run main.py "What are three uses of LLMs in production software?" Command-line argument handling and token logging are skills you will use in every project throughout the curriculum. Building them now makes later projects faster.

Knowledge Check

Answer each question from memory, then click to reveal.

Additional Resources