Production OpenAI SDK App

Ma Mahalakshmi V Updated 19 Sep 2026
9 min read ·Lesson 219 of 224

Project 10: Deploy a Complete OpenAI SDK Application to Production

This project takes Project 1's chatbot — ChatSession, the retry wrapper, and the streaming loop — and carries it the rest of the way to a production-ready deployment, synthesizing deployment and operations patterns from Unit 26, the security practices from Unit 23, and the observability, cost, and performance techniques from Unit 25. Where earlier projects each introduced one capability, this lesson is entirely about the layer that sits around a working application before it is safe and observable enough to run in front of real users.

Scope and Design Decisions

The starting point is assumed to be Project 1's ChatSession and stream_reply function, already working locally. This lesson adds four things a working prototype does not have: a secured API boundary, request-level authentication, structured logging and cost tracking, and a deployable service definition. It does not cover infrastructure choice (which cloud provider, which container platform) in depth — that decision is context-specific and Unit 26 covers the trade-offs; the focus here is what needs to be true of the application itself regardless of where it runs.

Four decisions define this project:

  1. Secrets never enter application code or logs. The API key and any other credentials are loaded from environment variables at startup and are never written to a log line, an error message, or a response body.
  2. Every request is authenticated before it reaches the model. A publicly reachable chatbot endpoint with no request-level authentication is an open invitation to have your API budget consumed by someone else's traffic.
  3. Cost and latency are logged per request, not inferred after the fact. Production incidents involving runaway spend or degraded latency are diagnosed far faster when the data was captured at request time rather than reconstructed later from provider billing dashboards.
  4. The service fails safely and visibly. An unhandled error should return a clean error response and emit a log entry, never leak a stack trace or an API key to the client.

Wrapping the Chatbot as a Secured HTTP Service

import os
import time
import logging
import hashlib
import hmac
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import StreamingResponse

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
logger = logging.getLogger("chatbot_service")

app = FastAPI()

API_KEY_HASH = os.environ["CHATBOT_API_KEY_HASH"]  # sha256 hex digest, never the raw key

def verify_request_key(provided_key: str) -> bool:
    computed_hash = hashlib.sha256(provided_key.encode()).hexdigest()
    # Constant-time comparison prevents a timing side-channel from leaking
    # how many leading characters of the hash matched.
    return hmac.compare_digest(computed_hash, API_KEY_HASH)

@app.middleware("http")
async def require_api_key(request: Request, call_next):
    provided = request.headers.get("x-api-key", "")
    if not verify_request_key(provided):
        logger.warning("Rejected request with invalid API key from %s", request.client.host)
        raise HTTPException(status_code=401, detail="Invalid or missing API key")
    return await call_next(request)

Storing API_KEY_HASH rather than the raw client-facing key means that even if the service's environment configuration were exposed, an attacker would have a hash, not a usable credential — the same principle as storing password hashes rather than passwords, applied here to service-to-service authentication. hmac.compare_digest is used instead of == specifically because Python's default string comparison short-circuits on the first mismatched character, which creates a timing side-channel: an attacker measuring response times could incrementally guess a hash byte by byte. This is a small detail with an outsized security consequence, and it is exactly the kind of thing Unit 23 emphasizes checking for in any comparison involving secrets.

Note: Real deployments typically use a proper API gateway, OAuth, or a managed authentication service rather than a hand-rolled header check; this middleware illustrates the underlying principle (verify before processing, compare secrets in constant time) in a form small enough to read in full, not a recommendation to hand-roll auth for a real production system.

Wiring in the Chatbot With Cost and Latency Logging

from openai import OpenAI

client = OpenAI()  # reads OPENAI_API_KEY from the environment automatically

# Reuse ChatSession, call_with_retry, and stream_reply from Project 1 unmodified.

PRICE_PER_1K_INPUT_TOKENS = 0.003
PRICE_PER_1K_OUTPUT_TOKENS = 0.012

def estimate_cost(usage) -> float:
    input_cost = (usage.input_tokens / 1000) * PRICE_PER_1K_INPUT_TOKENS
    output_cost = (usage.output_tokens / 1000) * PRICE_PER_1K_OUTPUT_TOKENS
    return round(input_cost + output_cost, 6)

@app.post("/chat")
async def chat_endpoint(request: Request):
    body = await request.json()
    user_text = body.get("message", "")
    session_id = body.get("session_id", "anonymous")

    session = _get_or_create_session(session_id)
    start_time = time.monotonic()

    try:
        response = client.responses.create(
            model=session.model,
            input=session.to_input_list() + [{"role": "user", "content": user_text}],
        )
    except Exception:
        logger.exception("Chat completion failed for session %s", session_id)
        raise HTTPException(status_code=502, detail="The assistant is temporarily unavailable.")

    latency_ms = round((time.monotonic() - start_time) * 1000, 1)
    cost = estimate_cost(response.usage)

    logger.info(
        "session=%s latency_ms=%s cost_usd=%s input_tokens=%s output_tokens=%s",
        session_id, latency_ms, cost, response.usage.input_tokens, response.usage.output_tokens,
    )

    session.add_user_message(user_text)
    session.add_assistant_message(response.output_text)
    return {"reply": response.output_text, "latency_ms": latency_ms, "cost_usd": cost}

_SESSIONS: dict[str, "ChatSession"] = {}

def _get_or_create_session(session_id: str):
    from project1_chatbot import ChatSession  # reused from Project 1
    if session_id not in _SESSIONS:
        _SESSIONS[session_id] = ChatSession(system_prompt="You are a helpful assistant.")
    return _SESSIONS[session_id]

estimate_cost computes a per-request dollar figure directly from response.usage, logged alongside latency on every single request rather than only sampled or reconstructed later from a monthly bill. This is the difference between noticing a cost spike within minutes of a bad deployment and noticing it at the end of the billing cycle — cheap to add, and one of the highest-leverage observability practices from Unit 25 for any LLM-backed service, since per-request cost varies far more than in typical web services and can spike suddenly from something as simple as a prompt change that grows the system prompt.

The try/except around the model call logs the full exception server-side with logger.exception (which includes the stack trace) but returns only a generic, safe message to the client — this is the fail-safely principle in code: detailed diagnostic information stays in the logs where an operator can see it, and the client never receives anything that could reveal internal implementation details or, worse, a fragment of a stack trace containing a partially-redacted secret.

Health Checks and Graceful Startup Validation

@app.on_event("startup")
async def validate_configuration():
    required_vars = ["OPENAI_API_KEY", "CHATBOT_API_KEY_HASH"]
    missing = [v for v in required_vars if not os.environ.get(v)]
    if missing:
        raise RuntimeError(f"Missing required environment variables: {missing}")
    logger.info("Configuration validated successfully at startup")

@app.get("/health")
async def health_check():
    return {"status": "ok"}

validate_configuration runs once at process startup and fails loudly — raising, which prevents the service from starting at all — rather than allowing the service to come up and fail confusingly on the first real request. This is a deliberate trade-off: a service that starts successfully but is silently misconfigured produces much harder-to-diagnose incidents than one that refuses to start with a clear error message naming exactly which variable is missing. /health is a minimal liveness endpoint, the kind a container orchestrator or load balancer polls to decide whether to route traffic to this instance; a more thorough version might also verify connectivity to the OpenAI API itself, at the cost of a small amount of latency on every health check.

Containerizing the Service

# Dockerfile content (not executable Python, shown for reference):
#
# FROM python:3.12-slim
# WORKDIR /app
# COPY requirements.txt .
# RUN pip install --no-cache-dir -r requirements.txt
# COPY . .
# ENV PYTHONUNBUFFERED=1
# EXPOSE 8000
# CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

def load_secrets_from_environment() -> dict:
    """Confirms the deployment pattern: secrets arrive via environment
    variables injected by the orchestration platform's secret manager,
    never baked into the image or committed to source control."""
    return {
        "openai_api_key": os.environ["OPENAI_API_KEY"],
        "api_key_hash": os.environ["CHATBOT_API_KEY_HASH"],
    }

The Dockerfile shown as a comment (since it is not Python, it cannot go in an executable block, but it belongs alongside the code it packages) never copies a .env file or hardcodes a key — secrets are injected at container runtime by whatever secret manager the deployment platform provides (a cloud provider's secrets service, Kubernetes secrets, or a platform like the one Unit 26 walks through). PYTHONUNBUFFERED=1 ensures log output is flushed immediately rather than buffered, which matters because a container's stdout is typically what the orchestration platform's log collector reads — buffered output can make logs appear to "disappear" until the buffer flushes, which is exactly the kind of thing to catch before it causes a confusing debugging session in production.

Testing the Security and Cost Logic Without a Live Server

def test_verify_request_key_accepts_correct_key():
    global API_KEY_HASH
    real_key = "test-secret-key-123"
    API_KEY_HASH = hashlib.sha256(real_key.encode()).hexdigest()
    assert verify_request_key(real_key) is True
    print("PASS: correct API key is accepted")

def test_verify_request_key_rejects_wrong_key():
    global API_KEY_HASH
    API_KEY_HASH = hashlib.sha256("correct-key".encode()).hexdigest()
    assert verify_request_key("wrong-key") is False
    print("PASS: incorrect API key is rejected")

class FakeUsage:
    def __init__(self, input_tokens, output_tokens):
        self.input_tokens = input_tokens
        self.output_tokens = output_tokens

def test_estimate_cost_computes_expected_value():
    usage = FakeUsage(input_tokens=1000, output_tokens=500)
    cost = estimate_cost(usage)
    expected = round((1000 / 1000) * 0.003 + (500 / 1000) * 0.012, 6)
    assert cost == expected
    print("PASS: cost estimate matches expected per-token pricing calculation")

test_verify_request_key_accepts_correct_key()
test_verify_request_key_rejects_wrong_key()
test_estimate_cost_computes_expected_value()

test_verify_request_key_rejects_wrong_key and its accepting counterpart validate the authentication boundary using plain hashing with no HTTP server, no middleware stack, and no network call — the security-critical logic (does this credential match) is fully isolated and testable, which is exactly what should be true of any authentication check, since it is the piece of the system where an untested edge case has the highest consequence. estimate_cost is tested against a FakeUsage object with known token counts and a hand-computed expected value, which protects against a future refactor accidentally breaking the pricing formula unnoticed — a change that would otherwise only surface as an unexplained discrepancy in a monthly bill weeks later.

Extending This Project

Add per-session-ID rate limiting on top of the API key check to prevent a single compromised credential from consuming the entire request budget, and add distributed tracing (OpenTelemetry) around the model call so latency and cost can be correlated with specific upstream requests in a multi-service deployment.

Common Mistakes

  • Comparing API keys or hashes with a standard ==. This is vulnerable to a timing attack that can leak information about the correct value byte by byte. Always use a constant-time comparison such as hmac.compare_digest for secret comparisons.
  • Letting the service start successfully with missing configuration. A service that starts but fails mysteriously on the first request is much harder to diagnose than one that refuses to start with an explicit error naming the missing variable.
  • Logging only aggregate cost from a monthly billing dashboard. By the time a cost anomaly shows up there, it has often been running for weeks. Log cost per request so a spike is visible within minutes of a bad deployment.

Best Practices

  • Load all secrets from the environment, never from source code or committed files. Combine this with a startup check that fails fast and clearly if anything required is missing.
  • Log latency and cost on every request, not as an afterthought. This is the single highest-leverage observability addition for an LLM-backed service, given how variable per-request cost can be.
  • Return generic, safe error messages to clients while logging full detail server-side. A client should never see a stack trace, an internal exception message, or anything that could reveal implementation details or partial secrets.

0 Comments

Reviewed before they appear

No comments yet.

OpenAI SDK
Introduction to the OpenAI SDK Setting Up Python Creating an API Key Your First Call — client.responses.create() and response.output_text Understanding Billing, Credits, and What a Request Costs Why Responses Replaced Chat Completions Anatomy of a Request: model, input, and instructions Anatomy of a Response: The Typed output Array, Not Just Text Roles: User, Assistant, and Developer/System Choosing a Model, and Reading the Models Page Instead of Memorizing Names Instructions vs. Input Writing Prompts That Get Consistent Results Few-Shot Examples Reasoning Models and the reasoning Parameter Debugging a Prompt That Misbehaves Why Streaming Matters for User Experience stream=True and Iterating Over Events Handling the Event Types You Actually Care About Background Mode for Long-Running Jobs Project — Add Live Streaming to Your Chatbot The Problem With Parsing Free Text JSON Schema and Strict Mode Pydantic Models With the SDK's Parse Helpers Handling Refusals and Validation Failures Project — A Resume-to-JSON Extractor Working With input_image input_file, PDFs, and the Files API Image Generation Speech-to-Text and Text-to-Speech Project: A PDF Question-Answering Script What Function Calling Is Defining a Tool Schema The Full Loop Multiple Tools Errors, Timeouts, and Untrusted Arguments Project: A Weather Assistant Web Search File Search and Vector Stores Code Interpreter Remote MCP Servers and Connectors Project: A Research Assistant What an Embedding Is, Without the Maths Generating and Storing Embeddings Similarity Search From Scratch Hosted Vector Stores vs. Rolling Your Own A Small RAG App Over a Folder of Notes Agents vs. a Single API Call — When You Need One pip install openai Giving Agents Tools Handoffs and Multi-Agent Triage Guardrails and Approvals Tracing and Observing What Your Agent Did A Multi-Agent Support Desk Error Codes and What Each One Means Retries, Timeouts, and Backoff Rate Limits and Spend Limits Prompt Caching and Cost Optimisation The Batch API for Bulk Work Async Clients and Concurrency Moderation and Safety Best Practices Designing the App Backend With FastAPI Streaming to a Simple Frontend Deploying and a Cost/Safety Checklist Why Web Search Is Useful for Current Information Using the Web Search Tool with the Responses API Configuring Search Behavior for Application Use Cases Understanding Citations and Source Attribution Where to Go Next Building a Research Assistant with Web Search Combining Web Search with Structured Outputs Handling Conflicting or Low-Quality Web Sources Reducing Unsupported Claims with Grounded Generation Testing Freshness-Sensitive AI Answers Production Considerations for Web-Grounded Applications Understanding File Search and Retrieval-Augmented Generation Creating and Organizing Vector Stores Uploading Documents for Retrieval Connecting Vector Stores to Responses API Requests Designing Document Metadata and Filtering Strategies Building a PDF Question-Answering Application Improving Retrieval Quality With Better Document Preparation Handling Missing Evidence and Retrieval Failures Combining File Search With Web Search Building a Production Knowledge-Base Assistant What the Code Interpreter Tool Is Designed For Running Python-Based Analysis Through the OpenAI SDK Uploading Datasets for Analysis Analyzing CSV and Spreadsheet Data Generating Charts and Data Summaries Handling Generated Files and Downloadable Artifacts Building a Data-Analysis Assistant Combining Code Execution with Structured Outputs Validating Generated Calculations and Results Security and Sandbox Considerations for Code Execution Understanding Multimodal Input with the OpenAI SDK Sending Images to a Model Image Analysis from URLs and Uploaded Files Extracting Text and Information from Screenshots Building an Image-Question-Answering Application Combining Image Input with Structured Output Analyzing Multiple Images in One Request Handling Image Quality and Input Limitations Designing Multimodal Prompts for Reliable Results Building a Practical Vision-Powered Python Application Understanding Speech-to-Text and Text-to-Speech Workflows Transcribing Audio with the OpenAI SDK Working with Uploaded Audio Files Handling Timestamps and Transcription Metadata Building a Meeting Transcription Workflow Generating Spoken Responses from Text Handling Long Audio and Processing Failures Combining Audio with Text and Tool Calling Building an End-to-End Python Voice Application What Embeddings Are and When to Use Them Generating Embeddings With the OpenAI API Preparing Text for Embedding Comparing Vectors With Cosine Similarity Building a Simple Semantic Search Engine in Python Storing Embeddings in a Database Metadata Filtering for Semantic Search Chunking Strategies for Better Retrieval Evaluating Semantic Search Quality Building a Document Similarity Application When Batch Processing Makes Sense Designing Large-Volume AI Processing Pipelines Using Asynchronous Python with the OpenAI SDK Running Concurrent Requests Safely Controlling Concurrency and Avoiding Rate Limits Tracking Batch Job Progress Handling Partial Failures in Bulk Workloads Retrying Failed Items Without Duplicating Successful Work Designing Resumable AI Processing Jobs Building a Production Batch-Processing Pipeline Batch Processing Makes Sense Large-Scale AI Processing Pipelines Async Python with OpenAI SDK Safe Concurrent Requests Concurrency & Rate Limits Batch Progress Tracking Partial Failure Handling Safe Retry Handling Resumable AI Jobs Production Batch Pipeline System–User Data Separation Reusable App Instructions Prompt Templates & Variables Extraction & Classification Prompts Summarization & Transformation Prompts Explicit Output Requirements Prompt Version Management Prompt Testing & Evaluation Reusable Python Prompt Library API Key Security Secure API Key Storage Secure Secret Management Prompt Injection Prevention Trusted vs. Untrusted Content Tool Argument Validation Sensitive Data Handling Secure Logging AI Action Authorization Production AI Security Checklist Why AI Applications Need Evaluation Beyond Unit Tests Unit Testing OpenAI SDK Integration Code Mocking API Responses in Python Tests Testing Structured Outputs Against Schemas Testing Tool-Calling Workflows Building a Small Evaluation Dataset Measuring Accuracy, Consistency, and Failure Rates Regression Testing Prompts and Model Changes Human Evaluation Versus Automated Evaluation Creating a Repeatable Evaluation Pipeline AI Request Monitoring Token Cost Management Usage Metrics Design Reducing Model Calls Prompt & Context Optimization Model Selection & Optimization AI Caching Strategies Interactive Latency Optimization Usage Dashboards & Budget Alerts Performance & Cost Checklist Every API Call Starts Fresh Fixing API Statelessness Server-Side Conversation Memory Limits of Response Chaining What We're Building Conversation Memory Challenges Preparing an OpenAI SDK Application for Deployment Environment-Specific Configuration for Development and Production Deploying a Python AI Service with Docker Container Health Checks and Startup Configuration Managing Secrets in Cloud Deployments Background Workers for Long-Running AI Tasks Queues and Asynchronous Job Architectures Scaling AI Workloads Horizontally Monitoring Production Incidents and Failures Production Deployment Checklist for OpenAI SDK Applications Reusable OpenAI Service Classes AI Client Dependency Injection Typed AI Responses Python Configuration Management AI Request Decorators Centralized AI Error Handling Clean SDK Abstractions Reusable OpenAI Utilities Internal AI Python Libraries SDK Integration Maintenance Production AI Chatbot Document Q&A System Web Research Assistant Customer Support Agent AI Data Analysis Assistant Image Analysis App Meeting Transcription & Summary Semantic Document Search Multi-Tool AI Agent Production OpenAI SDK App Why "It Looked Fine When I Tested It" Isn't Enough Timing Note Status Note Pre-Decision Status Note Current Availability Note
Ask about this post
AI Ask about this post

Ask questions about Production OpenAI SDK App and get answers drawn from it.

Signed-in readers only.