Course ES

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.