30–40 min read · Hands-on coding
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.
openai and python-dotenv installed, .env file set upA 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.
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.
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:
:free suffix and are permanently free with no daily cap.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-...
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.
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.
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.
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.
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:
role — who is speaking. For a single question you only need "user". Two other roles exist: "assistant" (the model’s previous replies) and "system" (background instructions) — both are introduced in Lesson 3 when you build multi-turn conversations.content — the text of that message.For a single question with no history, the messages list contains just one entry with role "user".
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
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.
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.
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:
ask) so the rest of your code does not know or care about HTTP details.RuntimeError with plain English messages — the caller does not need to import openai exceptions to handle them.if __name__ == "__main__" guard lets this file be imported by other modules without immediately making an API call.gpt-4o-mini, and prints the answer plus the token count.
ai-foundations project, edit main.pyimport sys and sys.argv[1] to read the question from the command lineTokens used: Ntry/except that catches AuthenticationError and prints a helpful messageuv 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.
Answer each question from memory, then click to reveal.
A token is the unit LLMs use to measure and process text — roughly four characters or three-quarters of a word. Tokens matter because LLM providers charge per token: the more text you send and receive, the more it costs. Token counts also determine whether your input fits within a model’s context window.
→ What is an LLM?The client is an OpenAI() instance — a Python object that holds your API key and knows how to send requests to OpenAI’s API. It is created once at the top of the script (or module) and reused for every API call. Creating it is not the same as making an API call.
Every call to client.chat.completions.create() requires a model name (which LLM to use) and a messages list (the conversation, as a list of dictionaries with role and content).
response.choices[0].message.content. The response contains a choices list (usually one item), and each choice contains a message object with a content field holding the text.
AuthenticationError, imported from the openai package. This is raised when the API key is missing, invalid, or revoked. Always catch it separately from generic OpenAIError so you can give the user a clear, actionable error message.