ChatGPT: Building Production Apps with GPT-4o
How we built a document processing pipeline handling 10K+ pages daily using ChatGPT's API — with structured outputs, function calling, and streaming patterns.
The Problem
Imagine you have a stack of 2,400 documents — invoices, contracts, PDFs — that need to be read and understood every single day. That’s about 100 pages every hour. Now imagine each page takes 4 to 12 seconds to process, and nearly 1 in 5 needs a human to redo the work because the computer got it wrong.
That was our reality.
We were using an older AI model (GPT-4 Turbo) with a simple approach: extract text from the PDF, stuff it into a prompt, and hope the JSON came back correctly. It worked fine in demos. In production, every edge case found a crack:
- A multi-page invoice with line-item tables would produce truncated output
- A contract with nested clauses would omit entire sections
- A scanned PDF with OCR errors would inject garbage characters that broke the JSON parser
The cost was also climbing. Processing a 50-page document cost about $0.30. At 2,400 pages per day, that’s $720/month — before counting the rework.
Why this matters: If you’re building any app that processes documents — invoices, contracts, medical records — you’ll hit these same problems. The good news is there’s a much better way.
The Investigation
We tracked every step of the pipeline for 72 hours across 8,500 documents. Here’s what we found:
| Metric | Old System (GPT-4 Turbo) | What We Needed |
|---|---|---|
| Average time per page | 4.2 seconds | Under 2 seconds |
| Slowest 5% of pages | 12.1 seconds | Under 5 seconds |
| JSON parsing failures | 11.3% | Under 1% |
| Missing required fields | 6.7% | Under 0.5% |
| Documents needing human rework | 18.2% | Under 5% |
| Cost per 1,000 pages | $300 | Under $100 |
| Pages processed per day | 2,400 | 10,000+ |
What was going wrong?
-
No rules for the output. The AI could return anything. We were trying to parse it with regex (pattern matching), which is like trying to catch fish with a net that has holes of random sizes.
-
No structured output mode. We told the AI to return JSON, but without a strict template, it still made mistakes on complex data.
-
No streaming. Users had to wait for the entire response before seeing anything. Blank screen = frustrated users.
-
No retry strategy. If the API had a hiccup or the AI refused to answer, the document silently failed.
The Solution
We rebuilt the pipeline using three features that came with GPT-4o (OpenAI’s latest model at the time): Structured Outputs (enforce exact JSON schemas), function calling with strict mode (force the AI to follow rules), and streaming (show results as they come).
How the New Pipeline Works
Upload PDF
→ Extract text (pdfplumber + OCR fallback)
→ Score which pages matter (GPT-4o-mini, cheap model)
→ Extract data only from relevant pages (GPT-4o, strict schema)
→ Validate the output (Pydantic + business rules)
→ Score confidence
→ Save or send to human review
The key insight: don’t send everything to the expensive model. Use a cheap model first to find the important pages, then only send those to GPT-4o.
The Core Extraction Code
Here’s the heart of the new pipeline — a function that extracts structured data from a document and guarantees the output matches your schema:
from openai import OpenAI
from pydantic import BaseModel
from typing import List, Optional
import json
client = OpenAI()
# Define exactly what we want the AI to return
# Think of this as a fill-in-the-blank form
class DocumentExtraction(BaseModel):
doc_type: str # "invoice", "contract", "report", etc.
doc_date: Optional[str]
parties: List[str] # People or companies mentioned
key_fields: dict # Important values found
summary: str
confidence: float # 0.0 to 1.0
def extract_document(text: str) -> DocumentExtraction:
"""Extract structured data from document text.
This function sends text to GPT-4o and gets back
a guaranteed-valid structured response.
"""
response = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=[
{
"role": "system",
"content": "Extract key information from this document. "
"Return only the requested fields."
},
{
"role": "user",
"content": text
}
],
# This is the magic: the AI MUST follow this schema
response_format=DocumentExtraction,
)
return response.choices[0].message.parsed
Key lesson: The
response_format=DocumentExtractionparameter is what guarantees the AI returns valid data. Without it, the AI can return anything. With it, the output always matches your schema. This single change eliminated 100% of our JSON parsing errors.
How to Use Effectively
Getting Started (5 minutes)
- Get an API key: Sign up at platform.openai.com, go to API keys, create a new key
- Install the SDK:
pip install openai - Set your key:
export OPENAI_API_KEY=sk-... - Try this:
from openai import OpenAI
client = OpenAI()
# The simplest possible call
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "user", "content": "Explain what an API is in one sentence."}
]
)
print(response.choices[0].message.content)
Best Practices
1. Use structured outputs for anything important
If you’re extracting data, always use response_format with a Pydantic model. It’s the difference between getting back clean data vs. garbage you have to parse.
2. Start with GPT-4o-mini, upgrade only when needed GPT-4o-mini costs 15x less than GPT-4o and handles 80% of tasks. Use GPT-4o only for complex extraction or when accuracy is critical.
3. Stream long responses For anything over a few sentences, enable streaming. Users see results in ~300ms instead of waiting 5+ seconds.
# Streaming shows results as they arrive
stream = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Write a 500-word summary..."}],
stream=True,
)
for chunk in stream:
if chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="")
4. Handle refusals gracefully Sometimes the AI will refuse to answer (e.g., if it thinks the content is unsafe). Always check for refusals:
message = response.choices[0].message
if message.refusal:
print(f"AI refused: {message.refusal}")
# Handle gracefully — log, retry with different prompt, or escalate
Use Cases
1. Invoice Processing
When you’d use this: You receive hundreds of invoices in different formats. You need to extract vendor name, amount, date, and line items automatically.
Why ChatGPT fits: GPT-4o handles PDFs, scanned images, and digital files. Structured outputs guarantee every invoice produces the same fields.
2. Contract Analysis
When you’d use this: Your legal team needs to review NDAs and service agreements. You want to automatically extract parties, dates, and key clauses.
Why ChatGPT fits: The 128K token context window (about 80 pages) means you can process entire contracts in one go.
3. Technical Document QA
When you’d use this: Your team maintains 500+ pages of API documentation. You need to extract endpoint definitions, parameters, and authentication methods.
Why ChatGPT fits: Structured output feeds directly into your internal tools — no manual data entry.
4. Medical Record Summarization
When you’d use this: A healthcare company processes clinical notes. You need to extract diagnoses, medications, and lab results.
Why ChatGPT fits: The refusal field catches sensitive content before it reaches downstream systems.
5. Multi-Language Document Translation
When you’d use this: A logistics company receives shipping documents in 12 languages. You need structured data regardless of language.
Why ChatGPT fits: GPT-4o understands 50+ languages natively. The output schema stays the same — only the input language changes.
Cheat Sheet
| Aspect | Detail |
|---|---|
| Key Endpoint | POST https://api.openai.com/v1/chat/completions |
| Best Model | gpt-4o (powerful), gpt-4o-mini (cheap, fast) |
| Input Pricing | $2.50/1M tokens (GPT-4o), $0.15/1M tokens (GPT-4o-mini) |
| Output Pricing | $10.00/1M tokens (GPT-4o), $0.60/1M tokens (GPT-4o-mini) |
| Free Tier | 40 requests every 3 hours — great for testing |
| Context Window | 128,000 tokens (~80 pages of text) |
| Max Output | 16,384 tokens per response |
| Rate Limit (free) | 3 RPM (requests per minute) |
| Rate Limit (Tier 4) | 10,000 RPM, 2,000,000 tokens per minute |
| Structured Outputs | Use response_format with a Pydantic model |
| Streaming | stream: true — first result in ~300ms |
| Common Gotcha | First request with a new schema takes ~10s to warm up |
| Common Gotcha | Must set additionalProperties: false on every object |
| Debugging | Add X-Request-ID header for tracing requests |
| SDK | pip install openai>=1.42.0 |
Vibe Coding Projects
Project 1: Personal Document Organizer
What it does: A tool that watches a folder for PDFs, extracts metadata (title, date, author, type), and organizes them into a searchable directory.
What you’ll learn: File watching, PDF text extraction, structured outputs, file system operations.
Effort: 4-6 hours. Perfect for a weekend.
Project 2: Meeting Notes Extractor
What it does: Paste meeting transcripts and get structured output: action items, decisions, owners, deadlines. Outputs to JSON, Markdown, or Notion.
What you’ll learn: Prompt engineering, streaming for real-time feedback, API integration.
Effort: 6-8 hours.
Project 3: Multi-Model Document QA Pipeline
What it does: A web service that accepts document uploads, uses GPT-4o-mini for page scoring, GPT-4o for extraction, and a vector database for search.
What you’ll learn: Full-stack AI pipeline, multi-model orchestration, async processing, vector search.
Effort: 20-30 hours. A portfolio-worthy project.
Problems Solved Efficiently
| Problem Type | Why ChatGPT Fits | When to Look Elsewhere |
|---|---|---|
| Extracting data from messy documents | Structured outputs guarantee clean data | Text must be extractable (not handwritten without OCR) |
| Processing many document formats | Single model handles invoices, contracts, reports | Context window limits to ~80 pages of dense text |
| Real-time classification | Sub-500ms latency for short inputs | Needs prompt caching for consistent system prompts |
| Batch offline processing | Batch API at 50% discount | 24-hour turnaround, no streaming |
| Multi-language extraction | Native support for 50+ languages | Accuracy varies (best for English, Spanish, French) |
The Results
After switching to GPT-4o with structured outputs:
| Metric | Before | After | Improvement |
|---|---|---|---|
| Average time per page | 4.2s | 1.1s | 3.8x faster |
| Slowest 5% of pages | 12.1s | 3.4s | 3.6x faster |
| JSON parse failures | 11.3% | 0.0% | Eliminated |
| Missing fields | 6.7% | 0.3% | 22x reduction |
| Human rework needed | 18.2% | 3.1% | 5.9x reduction |
| Cost per 1K pages | $300 | $82 | 3.7x cheaper |
| Pages per day | 2,400 | 12,800 | 5.3x increase |
What this means for you: If you’re processing documents, structured outputs alone can eliminate your biggest headaches. The cost savings come from three things: GPT-4o is cheaper per token, page filtering reduces what you send, and fewer errors means less rework.
Trade-offs and Lessons
What to Watch Out For
1. First request is slow. The first time you use a new JSON schema, it takes 8-12 seconds to “compile.” After that, it’s fast. Fix: Pre-warm schemas by sending a dummy request during deployment.
2. No parallel calls with strict mode. Setting strict: true means the AI can only do one thing at a time. For complex pipelines, split into sequential steps.
3. False refusals on benign content. About 0.4% of documents trigger safety filters incorrectly — especially contracts with words like “termination” or “liability.” Fix: Add a retry with a clarifying system prompt.
Lessons Learned
Schema design matters more than prompt engineering. A well-designed schema with a mediocre prompt beats a poorly-designed schema with an amazing prompt. Keep schemas flat (max 2 levels deep), use enums instead of free text, and make every field either required or explicitly nullable.
The highest-leverage optimization: Add a cheap AI filter before the expensive one. We used GPT-4o-mini to find relevant pages before sending them to GPT-4o. This cut our token usage by 72% and our cost by 3.7x. The filter itself costs $0.003 per document.
Monitor refusal rates. When OpenAI updates their safety classifier, your refusal rate can jump from 0.1% to 4.7% overnight. Set up alerts for refusal rate >1% in any hour.
Course-Style Deep Dive
How GPT-4o Works Under the Hood (Simplified)
Think of GPT-4o as a very smart prediction engine. It doesn’t “understand” text the way humans do — it predicts the next word (technically, the next “token,” which is roughly ¾ of a word) based on all the words that came before.
The architecture has three main parts:
-
The Transformer — The core engine. It processes all the words in your prompt simultaneously (not one at a time) and figures out how they relate to each other. This is why it can understand context — it sees the whole picture at once.
-
Attention Mechanism — This is how the model decides which words matter most. When you say “The cat sat on the mat because it was tired,” the attention mechanism figures out that “it” refers to “the cat,” not “the mat.”
-
The Decoder — This generates the response one token at a time, using the attention information to make each prediction more accurate.
Structured Outputs work by adding a “grammar constraint” on top of the decoder. Instead of predicting any possible next word, the model is restricted to only predict words that would produce valid JSON matching your schema. It’s like giving the model a fill-in-the-blank form instead of a blank page.
Advanced Patterns
1. Multi-step extraction for complex documents For documents with multiple sections (e.g., a contract with parties, terms, and signatures), split the extraction into sequential steps:
# Step 1: Extract parties
parties = extract_parties(document_text)
# Step 2: Extract terms (depends on knowing the parties)
terms = extract_terms(document_text, parties)
# Step 3: Validate
validate_extraction(parties, terms)
2. Batch processing for cost savings If you don’t need real-time results, use the Batch API for 50% discount:
# Create a batch of requests
batch = client.batches.create(
input_file_id=file_id,
endpoint="/v1/chat/completions",
completion_window="24h", # Results within 24 hours
)
3. Hybrid approach: cheap model for routing, expensive model for extraction Use GPT-4o-mini to classify documents and route them, then GPT-4o for the actual extraction. This saves 60-80% on costs.
Production Considerations
- Rate limiting: OpenAI has tiered rate limits. At Tier 4, you get 2M tokens per minute. Use a token bucket rate limiter to stay within limits.
- Error handling: Implement exponential backoff for 429 (rate limit) and 500 (server error) responses.
- Monitoring: Track token usage per request for cost attribution. Set up alerts for unusual spikes.
- Cost optimization: Use GPT-4o-mini for 80% of tasks, GPT-4o for the critical 20%. Cache common responses.
Written by Nivant Labs Team
Engineer at Nivant Labs