Skip to content
beginner Phase 3 · LLM API Fundamentals

Your First LLM API Call

Send a chat completion request and read the response envelope end to end.

35m
0 problems
Topic Progress 0%

The Smallest Request That Actually Works

A key, an account, and a model name are all you need to get text back. The first call matters because it teaches the contract you will reuse in every later integration: what you send, what comes back, and what each piece costs you. Send it over raw HTTP before reaching for an SDK so nothing hides the payload.

The request

import httpx, os

response = httpx.post(
    "https://api.openai.com/v1/chat/completions",
    headers={"Authorization": "Bearer " + os.environ["OPENAI_API_KEY"]},
    json={
        "model": "gpt-4o-mini",
        "messages": [
            {"role": "user", "content": "Explain idempotency in one sentence."}
        ],
    },
    timeout=30.0,
)
response.raise_for_status()
print(response.json()["choices"][0]["message"]["content"])

Input: Explain idempotency in one sentence.

Output: Idempotency means retrying a request produces the same server-side effect as sending it once, so a duplicate charge or booking cannot happen.

Two fields carry the whole request. model selects the weights that answer. messages is the conversation so far, and every entry is the same pair: a role string and a content string. Nothing else is required on this endpoint — no system prompt, no temperature, no token cap. The rules that decide who may speak when live in Messages, Roles, and Turn Structure.

timeout=30.0 is not decoration. Generation is slow, and without an explicit budget a stalled connection parks your worker indefinitely. Set it too low and you abort mid-generation, paying for output you never read.

Shape of the exchange

POST /v1/chat/completions
 |
 |-- request
 |     model          "gpt-4o-mini"
 |     messages       [ {role, content}, ... ]
 |
 +-- response
       choices[0].message.content    the text you wanted
       choices[0].finish_reason      why generation stopped
       usage.prompt_tokens           what you sent
       usage.completion_tokens       what you paid for

You will use choices[0] in almost every call you write. The array exists because some requests ask for multiple candidate completions; a normal chat call fills index 0 and leaves the rest empty.

Reading the Response Envelope

The generated string is the part users see. The two fields next to it are the parts that keep the system healthy, and skipping them is how truncated answers quietly reach production.

finish_reason tells you why the model stopped. Four values cover nearly every case:

finish_reason What happened What to do
stop The model ended its own answer Normal path
length Generation hit max_tokens and was cut off Raise the cap or send a follow-up turn
content_filter The provider blocked the output Treat as a policy error, never a retry
tool_calls The model wants to call a function Parse arguments and execute

A length result is the dangerous one. The text looks finished — sentences read cleanly right up to the cut — so a service that only reads content[0].message.content will ship half an answer. Check the field before you trust the string:

choice = response.json()["choices"][0]
text = choice["message"]["content"]
if choice["finish_reason"] == "length":
    raise RuntimeError("Answer truncated at the token limit: " + text[-80:])

usage reports prompt_tokens (your instructions and history, sent in) and completion_tokens (what the model generated). They are billed at different rates and they come from the same tokenizer, so counting characters instead of tokens will misprice your feature — How Tokenizers Work explains why the ratio varies by language and punctuation. In production these two numbers become an invoice line, which is what Token Usage Metering builds on top of this response.

Three Failures You Will Hit First

Every integration meets the same early failures. They arrive as HTTP status codes, and each one demands a different reaction — retrying all of them identically is the most common first bug.

Failure modes, worst to most confusing:

  • 401 Unauthorized — the key is missing, mistyped, or revoked. A configuration problem. Retrying cannot fix it; fail fast and alert, or your logs fill with the same error every request.
  • 429 Too Many Requests — you exceeded a per-minute rate limit or your prepaid quota ran out. Transient. Wait for the interval the response indicates, then retry with jittered backoff.
  • 400 Bad Request — the payload is malformed: an empty messages array, a role that does not exist, a model name that was never real. The response body carries a human-readable message; print it instead of guessing.
  • 5xx — the provider is having a bad minute. Retry with backoff, and cap total attempts so a long outage surfaces as an error rather than a hung queue.
import httpx, os, time

payload = {
    "model": "gpt-4o-mini",
    "messages": [{"role": "user", "content": "Ping"}],
}

for attempt in range(4):
    response = httpx.post(
        "https://api.openai.com/v1/chat/completions",
        headers={"Authorization": "Bearer " + os.environ["OPENAI_API_KEY"]},
        json=payload,
        timeout=30.0,
    )
    if response.status_code == 200:
        print(response.json()["choices"][0]["message"]["content"])
        break
    if response.status_code == 429:
        wait = float(response.headers.get("retry-after", 2 ** attempt))
        time.sleep(wait)          # provider tells you how long to back off
        continue
    if response.status_code == 400:
        raise ValueError(response.json()["error"]["message"])  # your bug, not theirs
    if response.status_code == 401:
        raise SystemExit("API key rejected; check OPENAI_API_KEY")
    time.sleep(2 ** attempt)      # 5xx and anything else: exponential backoff

Retrying is only half the story. The key itself must live in an environment variable or a secret manager, injected at runtime. It does not go in source control, in a client-side bundle where anyone can read the DOM, in a Docker image layer, or in a log line. Treat a leaked key as an incident: revoke it first, then investigate, then rotate. Rate Limiting and Retries covers the backoff policy in depth, and the SDK in OpenAI SDK Essentials automates most of the loop you just wrote by hand.

Quiz

1. A request returns HTTP 429 with a retry-after header. What is the correct response?

Question 1 options

2. The response contains readable text but finish_reason is `length`. What does that tell you?

Question 2 options

3. Why can an API key never be embedded in front-end code?

Question 3 options

4. Which field is strictly required to get a chat completion back from this API?

Question 4 options

Flashcards

Question

What are the two required fields of a chat-completions request?

Answer

`model` and `messages`; every message is a role string paired with a content string.

Question

How do you read the generated text, the stop reason, and the cost from a response?

Answer

`choices[0].message.content` for text, `choices[0].finish_reason` for why it stopped, `usage.prompt_tokens` and `usage.completion_tokens` for cost.

Question

Which status codes mean auth failure, rate limiting, and malformed request?

Answer

401 = invalid or revoked key (fix config, do not retry); 429 = rate or quota limit (back off and retry); 400 = payload is wrong (read the error message and fix the request).

Question

What does finish_reason `length` mean and why is it dangerous?

Answer

Generation was cut off at `max_tokens`. The text still reads naturally, so services that ignore finish_reason will ship a half-written answer.

Question

Where may an API key live, and what happens when one leaks?

Answer

In an environment variable or secret manager injected at runtime — never in source control, client bundles, image layers, or logs. On a leak: revoke first, then investigate, then rotate.

Revision Notes

Key Takeaways

  • 1. A chat request is just a model name plus a messages list where every entry pairs a role with content
  • 2. The response carries three things you must read: the text, the finish reason, and the token usage
  • 3. 401, 429, and 400 demand different handling — retrying all of them the same way is the classic first bug
  • 4. Completion tokens cost more than prompt tokens, so usage belongs in your product's billing path from day one
  • 5. An API key is a credential: server-side secret only, revoked first when it leaks

Interview Tips

  • • Be ready to write the full request and response parsing on a whiteboard — interviewers commonly ask what `finish_reason` tells you and what you do about `length`
  • • When asked how you handle errors, walk the 401/429/400/5xx split and mention honoring `retry-after` rather than blind retries
  • • Expect the security question: where does the key live, what if it ends up in a git commit, and how do you rotate it
  • • If asked about cost, distinguish prompt from completion tokens and say you would meter `usage` per request rather than estimate from characters

Cheat Sheet

Your First LLM API Call — Cheat Sheet

Minimal request

httpx.post(
    "https://api.openai.com/v1/chat/completions",
    headers={"Authorization": "Bearer " + os.environ["OPENAI_API_KEY"]},
    json={"model": "gpt-4o-mini",
          "messages": [{"role": "user", "content": "..."}]},
    timeout=30.0,
)

Read these three fields every time

  • choices[0].message.content → the answer
  • choices[0].finish_reason → stop ok · length truncated · content_filter policy · tool_calls parse args
  • usage.prompt_tokens / usage.completion_tokens → what it cost

Status code decision rule

Code Meaning Action
401 bad/revoked key fix config, never retry
429 rate or quota limit wait retry-after, retry with jittered backoff
400 malformed payload read error.message, fix request
5xx provider incident exponential backoff, cap attempts

Key handling checklist

  • OPENAI_API_KEY in env or secret manager, not in code
  • .gitignore anything that could hold it; scan history if it ships
  • revoke before investigating a leak
  • never ship a key to a browser

Pre-flight checks

  1. messages non-empty, roles valid
  2. timeout set explicitly (30s is a sane start)
  3. finish_reason inspected before trusting the text

Continue Learning

Before You Start

Next Topics

Related Reading