Reusable OpenAI Service Classes

Ma Mahalakshmi V Updated 19 Sep 2026
7 min read ·Lesson 200 of 224

Building Reusable OpenAI SDK Service Classes

When a project only calls the OpenAI SDK from one or two places, it is common to see code like this scattered directly inside route handlers, CLI commands, or notebook cells:

from openai import OpenAI

client = OpenAI()

def summarize(text: str) -> str:
    response = client.responses.create(
        model="gpt-5.6-terra",
        input=f"Summarize the following text in two sentences:\n\n{text}",
    )
    return response.output_text

This works fine for a single script. It stops working once a project grows to five, ten, or thirty places that need to call a model — a summarizer, a classifier, a chatbot handler, a moderation check, a data-extraction job. Each of these ends up creating its own client, choosing its own model string, and handling errors in a slightly different way. A service class is the standard software-engineering answer to this kind of duplication: a single class that owns the SDK client and exposes a small, purposeful set of methods that the rest of the application calls instead of touching the SDK directly.

What a Service Class Is

A service class is an object whose entire responsibility is to wrap one external dependency — here, the OpenAI SDK — behind a stable, application-specific interface. It is not a data model, and it is not a generic utility bag. It has:

  • A constructor that receives (or creates) the client and any configuration it needs.
  • A small number of public methods, each representing one thing the application needs to do with the model (summarize, classify_ticket, extract_entities).
  • No knowledge of how those methods are called (HTTP handler, background job, CLI) — it only knows about the task.
from openai import OpenAI


class SummarizerService:
    def __init__(self, client: OpenAI, model: str = "gpt-5.6-terra") -> None:
        self._client = client
        self._model = model

    def summarize(self, text: str, *, max_sentences: int = 2) -> str:
        prompt = (
            f"Summarize the following text in at most {max_sentences} "
            f"sentences:\n\n{text}"
        )
        response = self._client.responses.create(
            model=self._model,
            input=prompt,
        )
        return response.output_text

The rest of the application now depends on SummarizerService, not on openai.OpenAI or on prompt strings scattered across the codebase.

Why This Matters

Three concrete problems disappear once SDK calls are centralized in a service class:

  1. Single point of change. If the prompt wording, the model name, or the response-parsing logic needs to change, it changes in one file instead of in every call site that duplicated it.
  2. Testability. A service class can be instantiated with a fake client in tests, so application logic can be verified without making real network calls. This is the same dependency-injection pattern used for testing throughout this course — Lesson 2 builds on it directly.
  3. Consistent behavior. Retries, logging, default parameters, and error handling can be applied once, inside the service, instead of being re-implemented (or forgotten) at every call site.

Without this structure, a change like "switch the summarizer to a different model" or "add a retry on rate-limit errors" turns into a multi-file search-and-replace operation, which is exactly the kind of maintenance risk that service classes exist to prevent.

Designing the Interface Around the Task, Not the SDK

A common mistake is to create a service class that simply mirrors the SDK's own methods:

class BadClientWrapper:
    def __init__(self, client: OpenAI) -> None:
        self._client = client

    def create_response(self, **kwargs):
        return self._client.responses.create(**kwargs)

This adds a layer of indirection without adding any value — every caller still needs to know the SDK's parameter names, still needs to extract output_text itself, and still needs to duplicate prompt construction. A good service class method is named after what the application wants to accomplish (summarize, classify_ticket, translate) and hides the mechanics of turning that intent into an SDK call.

class TicketClassifierService:
    def __init__(self, client: OpenAI, model: str = "gpt-5.6-terra") -> None:
        self._client = client
        self._model = model

    def classify_ticket(self, ticket_text: str) -> str:
        response = self._client.responses.create(
            model=self._model,
            input=(
                "Classify the support ticket into exactly one category: "
                "billing, technical, account, or other.\n\n"
                f"Ticket:\n{ticket_text}\n\nCategory:"
            ),
        )
        category = response.output_text.strip().lower()
        allowed = {"billing", "technical", "account", "other"}
        return category if category in allowed else "other"

Notice that classify_ticket does something a raw SDK call does not: it normalizes and validates the output before returning it. This is exactly the kind of task-specific logic that belongs inside a service class, not repeated by every caller.

When to Introduce a Service Class

Introduce a service class as soon as a piece of model-calling logic is used from more than one place, or as soon as it needs to be unit tested. For a genuinely one-off script that calls the SDK exactly once and is never tested or reused, a service class is unnecessary ceremony — a plain function is enough. The decision is about reuse and testability, not about project size in lines of code.

Do not create one service class per SDK method (ResponsesService, EmbeddingsService) unless those really do correspond to independent application concerns. Instead, organize service classes around business capabilities (SummarizerService, SupportTicketService, DocumentExtractionService). This keeps the class names meaningful to someone reading the application's business logic, not just its infrastructure.

A service class can hold multiple related methods that share configuration and a client, as long as they belong to the same capability:

class DocumentService:
    def __init__(self, client: OpenAI, model: str = "gpt-5.6-terra") -> None:
        self._client = client
        self._model = model

    def summarize(self, text: str) -> str:
        response = self._client.responses.create(
            model=self._model,
            input=f"Summarize this document:\n\n{text}",
        )
        return response.output_text

    def extract_title(self, text: str) -> str:
        response = self._client.responses.create(
            model=self._model,
            input=f"Return only the best title for this document:\n\n{text}",
        )
        return response.output_text.strip()

If a class starts accumulating methods for unrelated capabilities (say, ticket classification alongside document summarization), that is a sign it should be split into two service classes. A useful rule of thumb: if you cannot describe the class's responsibility in one short sentence without using "and," it is doing too much.

Testing a Service Class Without Calling the Real API

Because the service class takes its client as a constructor argument, tests can supply a fake client that returns a fixed response, without any network access:

class FakeResponse:
    def __init__(self, output_text: str) -> None:
        self.output_text = output_text


class FakeResponsesAPI:
    def __init__(self, canned_text: str) -> None:
        self._canned_text = canned_text

    def create(self, **kwargs):
        return FakeResponse(self._canned_text)


class FakeClient:
    def __init__(self, canned_text: str) -> None:
        self.responses = FakeResponsesAPI(canned_text)


def test_classify_ticket_defaults_to_other_on_unknown_category() -> None:
    fake_client = FakeClient(canned_text="unknown-category")
    service = TicketClassifierService(client=fake_client)

    result = service.classify_ticket("My printer is on fire.")

    assert result == "other"
    print("PASS: classify_ticket falls back to 'other' for unrecognized output")


test_classify_ticket_defaults_to_other_on_unknown_category()

This test verifies the classifier's validation logic — the fallback to "other" when the model returns something unexpected — without depending on what the real model would actually say. The FakeClient, FakeResponsesAPI, and FakeResponse classes mimic just enough of the SDK's shape (client.responses.create(...).output_text) for the service class to work against them unmodified.

Common Mistakes

Instantiating the OpenAI client inside every method. Creating a new OpenAI() client on every call wastes connection setup and makes it impossible to inject a fake client for testing. Create the client once, in the constructor or at application startup, and pass it in.

Letting SDK-specific types leak out of the service. If summarize() returns the raw Response object instead of a plain string, every caller now needs to know about output_text and the SDK's response shape. Return plain Python types (str, dict, a small dataclass) so the rest of the application never needs to import openai at all.

One giant AIService class for the whole application. Cramming summarization, classification, translation, and embeddings into a single class produces a file that is hard to navigate and hard to test in isolation. Split by business capability instead.

Best Practices

Depend on an injected client, never a global one. Accept the client as a constructor parameter rather than importing a module-level client = OpenAI() inside the service file. This is what makes fake-client testing possible, and it is the foundation for Lesson 2's dependency injection pattern.

Keep methods named after intent, not mechanism. summarize, classify_ticket, and extract_title describe what the caller wants; they should never require the caller to know which SDK endpoint or prompt template is used underneath.

Return plain data, not SDK objects. Convert SDK response objects into plain strings, dicts, or typed models (see Lesson 3) at the boundary of the service class, so the rest of the codebase has zero dependency on the SDK's internal types.

Keep configuration (model name, defaults) as constructor parameters with sensible defaults. This lets production code use the default while tests and experiments override it explicitly, without editing the service class itself.

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 Reusable OpenAI Service Classes and get answers drawn from it.

Signed-in readers only.