API keys: headers and environment variables
A real model API refuses a request that does not say who is paying. The key travels in a header, and it belongs in an environment variable, never in your code.
Give model_api.py a key check. The imports gain Annotated, Header and HTTPException, and two pieces change:
API_KEY = "sk-local-123"
def check_key(authorization: str | None):
if authorization != f"Bearer {API_KEY}":
raise HTTPException(status_code=401, detail="missing or wrong API key")@app.post("/v1/chat/completions")
def chat(request: ChatRequest, authorization: Annotated[str | None, Header()] = None):
check_key(authorization)
question = request.messages[-1].content
answer = decide(question)
return {
"model": request.model,
"choices": [{"index": 0, "message": {"role": "assistant", "content": answer}}],
"usage": {"prompt_tokens": len(question.split()), "completion_tokens": len(answer.split())},
}Annotated[str | None, Header()] tells FastAPI to read authorization from the request's Authorization header instead of the query string. Bearer followed by the key is the usual way to send an API key, and it is what OpenAI and most other providers expect.
from fastapi.testclient import TestClient
from model_api import app
body = {"model": "local", "messages": [{"role": "user", "content": "I was charged twice"}]}
no_key = TestClient(app)
print(no_key.post("/v1/chat/completions", json=body).status_code)
with_key = TestClient(app, headers={"Authorization": "Bearer sk-local-123"})
print(with_key.post("/v1/chat/completions", json=body).status_code)headers= on the client adds that header to every request it sends, so the key is set once.
Keeping the key out of the code
A key written in a .py file ends up in version control, and from there in every copy of the project. Read it from an environment variable, a named value the terminal hands to every program it starts:
import os
from fastapi.testclient import TestClient
from model_api import app
key = os.environ["MODEL_API_KEY"]
model = TestClient(app, headers={"Authorization": f"Bearer {key}"})
body = {"model": "local", "messages": [{"role": "user", "content": "I was charged twice"}]}
response = model.post("/v1/chat/completions", json=body)
print(response.status_code, response.json()["choices"][0]["message"]["content"])export MODEL_API_KEY=sk-local-123
python call.pyexport sets the variable for this terminal. os.environ is a dictionary of every environment variable. Now forget to set it:
python call.pyA KeyError on the first line that needs the key is the right failure: loud, immediate, and naming the variable. os.environ.get with a default would hide the problem until the model API answered 401.
$env:MODEL_API_KEY = "sk-local-123".- Send
Bearer sk-wrongand print the response body. - Put
MODEL_API_KEY=sk-local-123in a file called.env, installpython-dotenv, and callload_dotenv()at the top ofcall.py. Add.envto.gitignore. - Print
key[:6] + "..."instead of the key when you need to check which one is loaded.
Every expert started right here.