Timing Note

Ma Mahalakshmi V Updated 19 Sep 2026
20 min read ·Lesson 221 of 224

Before You Build: A Timing Note Worth Knowing

Before writing any code, you need one piece of context that changes how you should read this lesson: OpenAI has announced that the Evals platform — the dashboard and the API this lesson teaches — is being wound down. According to OpenAI's own deprecations page, existing evals become read-only on October 31, 2026, and the platform (both dashboard and API) is scheduled to shut down on November 30, 2026. OpenAI's migration guidance points existing users toward third-party tools such as Promptfoo for ongoing evaluation work going forward.

That doesn't make this lesson pointless — quite the opposite. The Evals API is still live today, it's still the clearest, most explicit teaching example of what an evaluation pipeline actually consists of, and every concept you learn here — a structured dataset, testing criteria that define "correct," a run that executes your prompt against that dataset, and a scored result you can compare over time — is the exact same shape you'll find in Promptfoo, in a hand-rolled evaluation harness, or in whatever OpenAI replaces this platform with. Learn the concepts here, where the API makes every piece explicit and nameable, and you'll be able to apply the same thinking wherever you end up running evaluations after the shutdown date. If you're reading this after November 30, 2026, treat the workflow below as the conceptual blueprint and swap in your evaluation tool of choice — the ideas transfer directly.

What the Evals API Actually Does

An eval, in the OpenAI API, is a named, reusable configuration made of two things:

  1. A data source config — a JSON Schema describing the shape of your test data: what fields each test case has (an input, a correct label, maybe some metadata).
  2. Testing criteria — one or more graders that look at a model's output for a given test case and decide whether it's correct (Lesson 3 covers grader types in depth; this lesson uses the simplest one, string_check, so you can focus on the overall workflow first).

Once an eval exists, you don't run it directly. Instead, you create eval runs against it — each run points at a specific model, a specific prompt template, and a specific dataset file, executes the model against every row, grades every output with your testing criteria, and reports back pass/fail counts. This separation matters: the eval defines what correct means and what shape your data takes, and a run defines which model and prompt you're currently testing. That lets you run the same eval against gpt-4.1, then against a different prompt, then against a different model snapshot next month, and compare all three runs against the exact same bar.

Setup

You need the OpenAI Python SDK and an API key available as an environment variable.

Installation

pip install openai

.env

OPENAI_API_KEY=your_api_key_here

Never hard-code your API key directly in a script. Load it from the environment so it never ends up committed to source control:

import os
from openai import OpenAI

client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])

If you're using a .env file locally, load it with python-dotenv (pip install python-dotenv) and call load_dotenv() before reading the environment variable. In production, set the environment variable through your hosting platform's secrets manager instead of shipping a .env file at all.

The Example: Categorizing Support Tickets

To keep this concrete, we'll build an eval for a realistic feature: a function that reads the text of an incoming support ticket and classifies it into one of a fixed set of categories — billing, technical, account, or other. This is exactly the kind of feature Lesson 1 warned you about: it looks trivially correct on three hand-picked examples, and it can quietly misclassify a meaningful slice of real traffic without anyone noticing until a human audits a batch of tickets by hand.

Step 1: Define the Eval

The first step is telling the API the shape of your test data and how a correct answer will be judged. Each row in your dataset will have a ticket_text field (the input) and a correct_label field (the ground truth a human assigned).

eval_config = client.evals.create(
    name="Support Ticket Categorization",
    data_source_config={
        "type": "custom",
        "item_schema": {
            "type": "object",
            "properties": {
                "ticket_text": {"type": "string"},
                "correct_label": {"type": "string"},
            },
            "required": ["ticket_text", "correct_label"],
        },
        "include_sample_schema": True,
    },
    testing_criteria=[
        {
            "type": "string_check",
            "name": "Matches human-labeled category",
            "input": "{{ sample.output_text }}",
            "operation": "eq",
            "reference": "{{ item.correct_label }}",
        }
    ],
)

print(eval_config.id)

Walking through the important parts:

  • data_source_config describes your dataset's schema using JSON Schema, the same way you'd describe a structured output shape. type: "custom" means you're bringing your own data, as opposed to referencing a stored dataset OpenAI hosts for you.
  • include_sample_schema: True tells the eval that, in addition to your dataset's own fields (ticket_text, correct_label), each row will also have a sample object attached once a run actually executes — this is what lets your testing criteria reference {{ sample.output_text }}, the model's real output, rather than just the static dataset.
  • testing_criteria is a list of graders. Here we use one string_check grader with the eq (exact match) operation: it passes only if the model's raw output text is character-for-character identical to the correct_label field from that row. This is strict on purpose for a first example — Lesson 3 shows softer, more forgiving grading strategies for open-ended text.
  • The two template expressions, {{ sample.output_text }} and {{ item.correct_label }}, are how graders reference dynamic values. item always refers to a field from your dataset row; sample always refers to something produced by the model during the run.

client.evals.create() returns an eval object with an id — save it, because every run you create afterward needs to reference it.

Step 2: Build and Upload Your Test Dataset

Your dataset is a JSONL file — one JSON object per line — where each object matches the schema you just declared. This is where the advice from Lesson 1 about logging real production traffic pays off directly: the strongest datasets mix examples you write by hand for coverage with real (anonymized) examples pulled from logs.

tickets.jsonl

{"item": {"ticket_text": "I was charged twice for my subscription this month", "correct_label": "billing"}}
{"item": {"ticket_text": "The app crashes every time I try to upload a photo", "correct_label": "technical"}}
{"item": {"ticket_text": "I can't log in, it says my password is wrong but I just reset it", "correct_label": "account"}}
{"item": {"ticket_text": "Do you offer discounts for students?", "correct_label": "billing"}}
{"item": {"ticket_text": "Your export button doesn't work on Safari", "correct_label": "technical"}}
{"item": {"ticket_text": "I want to delete my account permanently", "correct_label": "account"}}
{"item": {"ticket_text": "What are your office hours?", "correct_label": "other"}}

A real dataset should have far more than seven rows — treat this as the minimum shape, not the target size. Aim for at least 50–100 rows for a first real eval, covering every category you care about and deliberately including the messy cases from Lesson 1: tickets that mix two topics, tickets in a casual or frustrated tone, tickets that are ambiguous even to a human reader. If two people on your team would label a ticket differently, either resolve the disagreement before adding it, or leave it out — an eval can't be more consistent than the ground truth you feed it.

Upload the file with the purpose set to "evals":

uploaded_file = client.files.create(
    file=open("tickets.jsonl", "rb"),
    purpose="evals",
)

print(uploaded_file.id)

Step 3: Create a Run

A run is where you actually specify the model and the prompt, and point the eval at your uploaded data:

run = client.evals.runs.create(
    eval_config.id,
    name="gpt-4.1 baseline",
    data_source={
        "type": "responses",
        "model": "gpt-4.1",
        "input_messages": {
            "type": "template",
            "template": [
                {
                    "role": "developer",
                    "content": (
                        "You are a support ticket classifier. Read the "
                        "ticket and respond with exactly one label: "
                        "billing, technical, account, or other. "
                        "Respond with only the label, nothing else."
                    ),
                },
                {"role": "user", "content": "{{ item.ticket_text }}"},
            ],
        },
        "source": {"type": "file_id", "id": uploaded_file.id},
    },
)

print(run.id, run.status)

Here, type: "responses" tells the run to use the Responses API to generate each output (the alternative is "completions" for the Chat Completions API). The template is a message list just like any normal Responses API call, except the user message contains {{ item.ticket_text }} — a placeholder the run engine fills in with that field from each row of your dataset before sending the request. The run executes this prompt once per row in tickets.jsonl, grades every output with the string_check criterion from Step 1, and aggregates the results.

Step 4: Retrieve and Read the Results

Runs execute asynchronously — for a large dataset, this can take a while, so you poll for status rather than blocking:

import time

while True:
    run = client.evals.runs.retrieve(run.id, eval_id=eval_config.id)
    if run.status in ("completed", "failed"):
        break
    time.sleep(5)

print(run.status)
print(run.result_counts)

result_counts gives you the headline numbers: how many rows passed, how many failed, how many errored (for example, if the model call itself threw an exception rather than returning an answer). This is the number Lesson 1 argued you can never get from manual testing — a precise, repeatable pass rate across every row in your dataset, not an impression from three anecdotes.

For a deeper breakdown, per_testing_criteria_results shows pass/fail counts broken down by each grader you defined (useful once you have more than one testing criterion), and report_url gives you a link to a dashboard view where you can inspect individual failing rows — which is usually where the real insight lives, since a single aggregate percentage tells you that something is wrong but not what.

Reading a Failure Correctly

Suppose your run comes back with 91 passed, 9 failed, out of 100. The temptation is to treat this as "9% bad, ship it anyway." Resist that until you've actually looked at which 9 rows failed. In practice, failures cluster. If all 9 failures are tickets that mention both a billing issue and a login problem in the same message, you've learned something specific and fixable: your prompt needs an instruction for how to handle ambiguous, multi-topic tickets, rather than a vague nudge to "try harder." That's a concrete, testable hypothesis you can turn into a prompt change — and then re-run the exact same eval to confirm the fix actually worked, instead of guessing.

Comparing Two Prompts With the Same Eval

The real payoff of separating the eval (what "correct" means) from the run (which model/prompt you're testing) is that you can run the exact same eval against two different prompts and get a head-to-head comparison, rather than two isolated impressions from two separate testing sessions.

Suppose the baseline run above scored 84 passed out of 100, and you suspect the model is struggling with tickets that mention a specific product name alongside a complaint. You revise the developer message to be more explicit about how to handle ambiguous or multi-topic tickets:

run_v2 = client.evals.runs.create(
    eval_config.id,
    name="gpt-4.1 with disambiguation instructions",
    data_source={
        "type": "responses",
        "model": "gpt-4.1",
        "input_messages": {
            "type": "template",
            "template": [
                {
                    "role": "developer",
                    "content": (
                        "You are a support ticket classifier. Read the "
                        "ticket and respond with exactly one label: "
                        "billing, technical, account, or other. "
                        "If a ticket mentions more than one issue, "
                        "classify it by the FIRST issue the customer "
                        "raises. Respond with only the label, nothing else."
                    ),
                },
                {"role": "user", "content": "{{ item.ticket_text }}"},
            ],
        },
        "source": {"type": "file_id", "id": uploaded_file.id},
    },
)

Because both runs point at the same eval ID and the same dataset file, their result_counts are directly comparable — same test cases, same grading logic, only the prompt changed. If run_v2 comes back at 93 passed out of 100, you have actual evidence the disambiguation instruction helped, not just a feeling that the new wording "reads better." If it comes back at 80, you've learned the new instruction made things worse in a way three manual tests would never have shown you, and you can revert with confidence instead of guesswork. This is the workflow you'll repeat constantly once evals are part of your process: change one thing, re-run, compare the number, keep or revert.

What's Actually Happening Behind the Scenes

It helps to understand the run as three distinct phases, even though the API handles all three for you automatically:

  1. Templating — for every row in your dataset file, the run engine substitutes {{ item.* }} placeholders in your prompt template with that row's actual field values, producing one fully-formed prompt per row.
  2. Generation — each filled-in prompt is sent to the model you specified (gpt-4.1 in the examples above), and the response is captured as that row's sample, including sample.output_text.
  3. Grading — each testing criterion you defined on the eval runs against every row, comparing whatever it's configured to compare (here, sample.output_text against item.correct_label) and recording a pass or fail.

Only after all three phases finish for every row does result_counts become final, which is why a run's status moves through an in-progress state before landing on completed. Understanding this three-phase shape also makes debugging much easier: if something's wrong, the first question is which phase failed — did templating substitute the wrong value, did the model produce unexpected output, or did the grader logic have a bug? The report_url view lets you inspect all three for any individual row, which is usually faster than guessing from the aggregate numbers alone.

Real-World Use Case: Catching a Regression Before a Deploy

Consider how this fits into an actual team's workflow rather than a one-off exercise. A support tooling team keeps their ticket classifier's eval — the one built in this lesson, grown over months to 300 real, anonymized examples — wired into their continuous integration pipeline. Every pull request that touches the classifier's system prompt automatically triggers a new eval run via a small script that calls client.evals.runs.create(), polls for completion, and fails the CI check if the pass rate drops by more than two percentage points compared to the current production prompt's last recorded score.

When an engineer later tweaks the prompt to fix an unrelated complaint about verbose responses, the CI check flags a drop from 94% to 89% before the change is ever merged. Looking at the failing rows through the report URL shows the new wording accidentally made the model less confident about the other category, causing it to default to technical more often. The engineer adjusts the instruction, reruns, gets back to 95%, and merges — a regression that would have shipped silently under a "looked fine when I tested it" process is caught in minutes, with zero manual reading of transcripts. This is the practical destination this entire lesson has been building toward: an eval isn't a one-time exercise you run once and forget, it's a permanent, automatable gate your team relies on the same way it relies on any other CI check.

Common Mistakes

Schema mismatch between the eval definition and the JSONL file. If your item_schema requires correct_label but a row in your JSONL file is missing it, that row will error rather than run. Validate your JSONL against your schema locally before uploading, especially for larger datasets assembled from scripts.

Forgetting include_sample_schema: True. Without it, your testing criteria can't reference {{ sample.output_text }} at all, because the eval doesn't know a sample object will exist. This is an easy one-line miss that produces a confusing template error when you create the run.

Grading on exact string match for open-ended output. string_check with eq works well for fixed-label classification like the example above, but it will fail almost everything for a summarization or open-ended writing task, where two correct answers can be worded completely differently. That's a mismatch between the task and the grader, not a real quality problem — Lesson 3 covers softer grading approaches (text_similarity, score_model) built for exactly this case.

Treating the dataset as a one-time artifact. Datasets should grow. Every time you find a real production failure, the fix isn't just to patch the prompt — it's to also add that exact failing case to your dataset, so a future change can never silently reintroduce the same bug without the eval catching it.

Troubleshooting

Error: the run stays in in_progress far longer than expected. Large datasets and heavier models take real time to process row by row. If a run seems stuck well past what dataset size would explain, retrieve it again to confirm the exact status rather than assuming — transient API-side delays are more common than actual hangs.

Error: rows show as errored instead of failed. A failed row means the grader ran and decided the output was wrong. An errored row means something broke before grading could happen — usually a malformed template reference, a model call that hit a rate limit, or a row that doesn't match your declared schema. Check the specific error message on that row via the report URL rather than treating it the same as a normal grading failure.

Unexpectedly low pass rate on a prompt you're confident in. Double-check the exact grader operation. eq requires a character-for-character match, including case and punctuation. If your model sometimes answers "Billing" instead of "billing", that's a grader strictness problem, not a model quality problem — either tighten your prompt's output instructions or switch to the case-insensitive ilike operation covered in Lesson 3.

client.evals.runs.create() raises a validation error about the template. This usually means a {{ item.field_name }} reference in your prompt template doesn't match a field name declared in item_schema when you created the eval. The two have to agree exactly — if your dataset uses ticket_text but your template references {{ item.ticket }}, the mismatch surfaces as a template error at run-creation time rather than a silent failure later, which is a helpful early signal that something in your schema and your prompt have drifted apart.

The file upload succeeds, but every row errors out during the run. Open the JSONL file and confirm every line is valid, standalone JSON — a single misplaced comma or an unescaped quote in one line is enough to make the parser reject that row, and depending on how the file was generated, a bug in one row's formatting can sometimes repeat across every row if it came from the same buggy export script. Validating the file locally with a small script that calls json.loads() on every line before uploading catches this in seconds, well before you've spent API calls generating outputs for rows that were never going to grade correctly.

A short validation script is worth keeping around permanently rather than writing ad hoc each time:

import json

def validate_jsonl(path: str, required_fields: set[str]) -> None:
    with open(path) as f:
        for line_number, line in enumerate(f, start=1):
            line = line.strip()
            if not line:
                continue
            try:
                row = json.loads(line)
            except json.JSONDecodeError as e:
                raise ValueError(f"Line {line_number}: invalid JSON ({e})")
            item = row.get("item", {})
            missing = required_fields - item.keys()
            if missing:
                raise ValueError(f"Line {line_number}: missing fields {missing}")

validate_jsonl("tickets.jsonl", required_fields={"ticket_text", "correct_label"})

Running this before every upload turns a class of run-time errors that used to surface only after burning API calls into an instant, local, zero-cost check.

Best Practices

Start with a small, high-quality dataset rather than a huge, noisy one — 50 carefully labeled examples you trust are worth more than 500 you're not sure about, because a noisy dataset produces a noisy, untrustworthy pass rate. Version your dataset file the same way you version code, so you can tell exactly which set of test cases a given historical run was scored against. Re-run the same eval every time you change the prompt, the model, or a tool definition, and keep a simple record — even a spreadsheet — of the pass rate over time, so a regression shows up as a visible drop rather than a mystery. And resist the urge to keep tweaking the prompt until the eval hits 100%; a dataset with a few permanently hard, ambiguous cases is often more honest than one you've quietly shaped to be easy to pass.

It's also worth deciding, upfront, who owns adding new rows to the dataset. On the team described above, the convention is simple: any time a real misclassification reaches a human — through a bug report, a support escalation, or a manual audit — that exact ticket text and its correct label get added to tickets.jsonl before the underlying prompt bug is even fixed. This means the dataset grows monotonically more representative of real failure modes over the life of the feature, instead of staying frozen at whatever a developer imagined on day one. A team that only edits the dataset when someone "gets around to it" tends to end up with an eval that looks reassuring but quietly stops reflecting reality.

Suggested Project Structure

For anything beyond a single throwaway script, it's worth splitting eval setup from eval execution, so you're not recreating the eval definition (and accidentally generating a new eval ID) every time you want to run it:

ticket-classifier-eval/
├── .env
├── requirements.txt
├── tickets.jsonl          # your test dataset
├── setup_eval.py          # creates the eval + uploads the file, run once
└── run_eval.py            # creates a run against the existing eval, run often

setup_eval.py contains the client.evals.create() and client.files.create() calls from Steps 1 and 2, and should print the resulting eval.id and file.id so you can save them (as environment variables or in a small config file) rather than hard-coding them inline everywhere. run_eval.py reads those saved IDs and only performs Steps 3 and 4 — creating a run and polling for results — which is the part you'll actually execute repeatedly, every time you touch the prompt. Keeping these concerns separate also makes it straightforward to wire run_eval.py into a CI pipeline later, since it becomes a small, self-contained script with a clear exit condition: pass if the pass rate meets your bar, fail otherwise.

Frequently Asked Questions

Do I need a separate eval for every prompt variant I want to test? No — this is precisely what runs are for. Create one eval per task (one eval for ticket classification, a separate eval for a different feature entirely), and create a new run every time you want to test a different prompt or model against that same task. Creating a brand-new eval for every prompt tweak throws away your ability to compare runs against each other, since result_counts is only meaningful when every run being compared used the same dataset and the same grading criteria.

Can I test a model I don't call through the Responses or Chat Completions API — like a fine-tuned model? Yes. The model field in a run's data_source accepts a fine-tuned model ID (the ft:... identifiers covered in Lesson 5) exactly the same way it accepts a base model name like gpt-4.1. This is, in fact, one of the most useful applications of an eval: running the same dataset and grading criteria against a base model and a fine-tuned model to get an apples-to-apples comparison of whether fine-tuning actually improved anything, instead of relying on a subjective impression.

Can I have more than one testing criterion on the same eval? Yes — testing_criteria accepts a list, and a run reports pass/fail separately for each one via per_testing_criteria_results, while result_counts reflects the combined outcome. This is useful even before you get to the multi grader covered in Lesson 3: for the ticket classifier, you might add a second, independent criterion checking that the output contains no extra text beyond the label itself, so a technically-correct-but-verbose answer ("billing, I think") doesn't get silently counted the same as a clean one.

What happens if I need to update the dataset after the eval already has runs against it? Upload a new file with client.files.create() and reference the new file's ID in your next run's source. Previous runs stay tied to the file they were originally run against, so your historical results remain valid and comparable to each other — only runs going forward will be graded against the updated dataset. This is also a natural point to give your dataset file a version marker in its filename (tickets_v2.jsonl) so it's obvious from the file list alone which version any given run used.

Lesson 3 goes deep on the grader types only briefly introduced here — how to score open-ended text, how to use a second model as a judge of subjective quality, and how to write fully custom Python grading logic for cases no built-in grader handles well.

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 Timing Note and get answers drawn from it.

Signed-in readers only.