Python for JS Devs · LLM APIs

Calling an LLM API from FastAPI

The payoff. Everything so far — type hints, Pydantic, async, DI, the blocking trap — comes together in one LLM-backed endpoint.

Lesson 19Time ~15 min Win A working /chat endpoint backed by ClaudePrereq Lessons 10, 12, 16, 17

You'll use Anthropic's official Python SDK (anthropic). The same shape applies to OpenAI's SDK. We default to Claude Opus 4.8 (model id claude-opus-4-8), the current most capable model.

1 · Install & the smallest call

uv add anthropic

Set your key as an env var (the SDK reads ANTHROPIC_API_KEY automatically — never hardcode it):

# .env
ANTHROPIC_API_KEY=sk-ant-...
Node (mental model)
const client = new Anthropic();
const msg = await client.messages
  .create({ model, max_tokens, messages });
Python
import anthropic
client = anthropic.Anthropic()   # reads ANTHROPIC_API_KEY

msg = client.messages.create(
    model="claude-opus-4-8",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Hello!"}],
)
Reading the response — a real gotcha msg.content is a list of content blocks, not a string. Grab the text safely:
text = next(b.text for b in msg.content if b.type == "text")

2 · Use the ASYNC client in FastAPI (Lesson 17 pays off)

An HTTP call to an LLM is I/O that takes seconds. In an async def route you must use the async client and await it — a blocking call here would freeze the event loop for every other request.

import anthropic
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()
client = anthropic.AsyncAnthropic()   # the async client

class ChatIn(BaseModel):
    message: str

class ChatOut(BaseModel):
    reply: str

@app.post("/chat", response_model=ChatOut)
async def chat(body: ChatIn):
    msg = await client.messages.create(     # awaited → loop stays free
        model="claude-opus-4-8",
        max_tokens=1024,
        messages=[{"role": "user", "content": body.message}],
    )
    reply = next(b.text for b in msg.content if b.type == "text")
    return ChatOut(reply=reply)

Every earlier lesson is visible here: Pydantic models validate in/out (12, 13), the async client cooperates with the loop (10, 17), and the API key comes from typed settings (16).

3 · Config & errors, the right way

Production shape Load the key via pydantic-settings (Lesson 16) so startup fails fast if it's missing. Wrap calls to catch SDK errors and map them to clean HTTP responses (Lesson 15):
try:
    msg = await client.messages.create(...)
except anthropic.RateLimitError:
    raise HTTPException(status_code=429, detail="Slow down")
except anthropic.APIStatusError as e:
    raise HTTPException(status_code=502, detail="LLM upstream error")

🎯 Your win

Build the POST /chat endpoint end-to-end: install anthropic, set ANTHROPIC_API_KEY, use AsyncAnthropic in an async def route, and call it from /docs with a real message. Getting a model reply back through your own typed, validated endpoint is the whole mission arriving.

Check yourself

Q1. Inside an async def FastAPI route, you should use:
The async client cooperates with the loop; a sync call would block it (Lesson 17).
Q2. The message's .content attribute is:
It's a list of blocks; filter for b.type == "text" and read b.text.
Q3. The API key should be provided by:
The SDK reads ANTHROPIC_API_KEY from the env; load via pydantic-settings and never hardcode.
Primary source Anthropic Python SDK and Claude API docs. (OpenAI's SDK follows the same async pattern.)
💬 Ask your teacher: how do I keep multi-turn conversation history, and where should the LLM call live in my layered structure (Lesson 16)?