Chapter 2 of 21 · basic
First API call
What this session covers
I've spent years watching a first AI program die on a missing or hardcoded key long before it ever reaches the model. So week one is deliberately small: by the end of the hour you've gotten a real answer back from a model on your own machine, twice — once through OpenAI, once through Google Gemini. That's it. No framework, no agent, no retrieval. Those come later, and every one of them assumes this part already works and never mentions it again.
I wrote the two examples to the same shape on purpose. Same import pattern, same key check, same one-prompt-one-answer flow. Read them back to back and the only things that move are the parts that genuinely differ between the SDKs. Those differences are the lesson. Everything else is scaffolding you'll stop noticing by week three.
OpenAI
OpenAI's example goes through the Responses API: hand responses.create a
model and an input string, read the text off response.output_text. The
key never appears in the code — OpenAI() with no arguments finds
OPENAI_API_KEY in the environment itself. The load_dotenv / find_dotenv
line is what makes that work without you exporting anything by hand. It walks
up from this file, finds the .env at the course root, and loads it before
the client gets built.
"""Week 1 - First API call (OpenAI).
The smallest program that does real work: read a key from the
environment, send one prompt, print one answer.
"""
import os
import sys
from dotenv import find_dotenv, load_dotenv
from openai import OpenAI
# Pull OPENAI_API_KEY from the course-root .env. find_dotenv walks up
# the directory tree, so this works no matter where you run it from.
load_dotenv(find_dotenv())
if not os.environ.get("OPENAI_API_KEY"):
sys.exit("OPENAI_API_KEY is not set. Put it in the course-root .env file.")
# OpenAI() reads OPENAI_API_KEY from the environment by itself - you
# never pass the key in code.
client = OpenAI()
response = client.responses.create(
model="gpt-5.4-nano",
input="In one sentence, greet someone taking their first AI course.",
)
print(response.output_text)
The guard in front of the client isn't decoration. Skip it and the SDK constructs fine, then dies deep inside an HTTP call with a traceback that never says "you forgot your key." Three lines turn that into one sentence you can act on. Every week's code in this book does the same thing, and I'd keep the habit well past this book.
Gemini
Same skeleton. What's worth noticing is where it diverges: the import is
from google import genai, the call is client.models.generate_content with
contents instead of input, and the text comes back on response.text
instead of response.output_text. The one thing I changed on purpose is the
key. OpenAI's client discovers OPENAI_API_KEY implicitly; here I pass it in
with api_key=.... Gemini's client would auto-discover GEMINI_API_KEY or
GOOGLE_API_KEY too, but on day one I'd rather see where the key comes from
than save a line.
"""Week 1 - First API call (Gemini).
Deliberately the same shape as the OpenAI example so you can read the
two SDKs side by side: key from the environment, one prompt, one answer.
"""
import os
import sys
from dotenv import find_dotenv, load_dotenv
from google import genai
# Same .env lookup as the OpenAI example.
load_dotenv(find_dotenv())
if not os.environ.get("GEMINI_API_KEY"):
sys.exit("GEMINI_API_KEY is not set. Put it in the course-root .env file.")
# Unlike OpenAI(), we hand the key in explicitly here. The client also
# auto-discovers GEMINI_API_KEY / GOOGLE_API_KEY, but being explicit
# keeps the first lesson unambiguous.
client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
response = client.models.generate_content(
model="gemini-3.1-flash-lite",
contents="In one sentence, greet someone taking their first AI course.",
)
print(response.text)
Two SDKs doing the same job, separated by three naming differences and one decision about key handling. For a call this simple, that's the entire comparison. Those same spots — the call name, the response field, how the client finds credentials — are what you'll keep checking as the two providers drift apart in later weeks.
Put it to work
A "hello, model" call is useful for about ten minutes. Three small web apps
turn the same two-line API into something I'd actually keep open in a tab.
Each one is its own docker-compose project under code/showcase/<slug>/ —
bash bootstrap-secrets.sh, docker compose up --build, open
http://localhost:3000. The chapter prints only the AI piece of each one,
ai_openai.py and ai_gemini.py. The Dockerfile, the FastAPI loader, and
the tiny Next.js form are scaffolding you run; the per-showcase README
covers them if you want the tour.
Showcase 1 — Explain a snippet
Paste code or a shell one-liner; get one paragraph of plain-English
explanation back. The trick is the prompt, not the SDK: "Explain in one
short paragraph, prose only, do not include code" pins the model to one
shape of answer, which is what turns a chat-style toy into something you
can pipe to. Try it on a regex you forgot, on a find invocation, on a
SQL query you inherited.
Showcase 2 — Rewrite tone
Paste a draft message, pick a tone — neutral, friendly, formal, terse —
get it rewritten preserving every fact. The frontend encodes the tone into
the single-string input as <tone>\n---\n<message>, which is how the
def run(input: str) -> str contract stays the same across all three
showcases in the week.
Showcase 3 — Draft a README
A one-paragraph project description in, a README skeleton out: What, Install, Usage, License. Drop it into a new repo, fix the placeholders, ship. Still one API call.
The same backend/main.py lives in all three projects, byte for byte.
It reads PROVIDER from the environment and importlibs either
ai_openai or ai_gemini. Want the Gemini answers instead?
PROVIDER=gemini docker compose up --build and refresh. The backend
never knew there was more than one SDK; the loader did. None of these
three are toys, and none of them used anything past the one API call
you wrote at the top of this chapter.
Run it
The README in this folder has the Python version, the install command, the
two environment variables, and the exact command to run each example. The
code expects an untracked .env at the course root. That file is git-ignored,
so the key never lands in version control, which is the whole point of not
hardcoding it.
Takeaways
The model call is two lines. Everything around it — loading .env, checking
the key, picking the model name — is what actually decides whether your
program runs or dies with a confusing traceback, and it's exactly the part
most tutorials skip. I'm not skipping it. Make this scaffold reflexive now:
key in the environment, never in the source; load it before the client; fail
loud and early when it's missing. Every later chapter stands on this and won't
stop to re-explain it.