General
The OpenAI Agents SDK in Production: A Complete Field Guide
Every capability, every provider, and the production playbook nobody hands you — including how to take the exact same agent code from OpenAI to vLLM, NVIDIA Triton, Together AI, and Azure AI Foundry.
When teams sit down to build their first serious agentic system, a strange thing happens: most of them reach for the OpenAI Agents SDK first. Not because it's the only option — LangGraph, CrewAI, Autogen, and a dozen others exist — but because it gets out of your way. It is small, typed, unopinionated about your infrastructure, and provider-agnostic by design. You can prototype an agent in ten lines and still ship the same code to production behind your own GPUs.
This guide is the long version of that promise. We'll walk through every major capability of the SDK, then spend the second half on the part that actually matters: running it in production. Along the way we'll cover the scenario that motivates so many adoptions — building an MVP on OpenAI and later moving the model on-prem to a vLLM or Triton backend, or to Azure AI Foundry, without rewriting your agents.
The code in this guide targets the current
openai-agentsPython package. Install it withpip install openai-agents(Python 3.10+).
Part 1 — Why this SDK keeps winning the first decision
Before the API tour, it's worth being precise about why the Agents SDK is the default starting point for so many projects.
It's a thin orchestration layer, not a framework that owns your app. The two primitives — Agent and Runner — manage the turn loop (call the model, run tools, follow handoffs, enforce guardrails, persist sessions). Everything else is plain Python objects you can inspect, clone, and test.
It's provider-agnostic. It speaks the OpenAI Responses API and Chat Completions API natively, and through one line of configuration it talks to any OpenAI-compatible endpoint, plus 100+ models via adapters. This is the feature that de-risks the whole project: your model choice is not a one-way door.
It's typed and small. Pydantic-backed schemas, real generics on context, and a tiny surface area mean your IDE and type checker do real work for you.
Tracing is built in. From the first run you get a structured trace of model calls, tool calls, handoffs, and guardrails — locally and in the OpenAI Traces dashboard — with first-class integrations for Logfire, Langfuse, Braintrust, Datadog, W&B, MLflow, and many more.
Production concerns are first-class, not afterthoughts. Retries, timeouts, human-in-the-loop approvals, durable execution integrations (Temporal, Dapr, Restate, DBOS), and zero-data-retention controls all ship in the box.
Keep these five points in mind — the rest of the guide is really just them, expanded.
Part 2 — The core: Agents and the Runner
An Agent
An Agent is an LLM configured with instructions, tools, guardrails, handoffs, and an optional structured output type.
The agent loop
When you call Runner.run(), the SDK runs a loop:
Call the LLM for the current agent with the current input.
Inspect the output:
If it's a final output (text of the desired type with no tool calls), stop and return the
RunResult.If it's a handoff, switch the current agent and re-loop.
If there are tool calls, execute them, append the results, and re-loop.
If the loop exceeds
max_turns, raiseMaxTurnsExceeded(passmax_turns=Noneto disable).
There are three entry points:
Runner.run(...)— async, returnsRunResult.Runner.run_sync(...)— synchronous wrapper.Runner.run_streamed(...)— async, returnsRunResultStreaming; you consume.stream_events()as the model produces tokens and items.
Agent configuration worth knowing
instructions can be a static string or a function (dynamic instructions) that receives context and the agent, so you can inject the user's name, locale, or the current date per run.
output_type turns plain text into a typed, structured output (a Pydantic model, dataclass, TypedDict, etc.). Under the hood this enables structured outputs.
model_settings carries temperature, top_p, tool_choice, parallel_tool_calls, and more.
tool_use_behavior controls whether tool results loop back to the model ("run_llm_again", the default), stop at the first tool ("stop_on_first_tool"), stop at named tools (StopAtTools(...)), or run a custom function that decides.
clone() duplicates an agent with overrides — perfect for A/B variants.
The run result: what Runner actually returns
This is the part most tutorials skip, so let's be precise. Runner.run() / run_sync() return a RunResult; Runner.run_streamed() returns a RunResultStreaming. Both inherit from RunResultBase, which exposes the surfaces you'll use every day:
Property / helper | What it holds | Use it for |
|---|---|---|
| The last agent's output: a | The answer you show the user |
| Rich | Logs, UIs, audits, debugging |
| An input-item view of the whole run, ready to feed into the next turn | Manual conversation loops |
| The agent that finished the run (often the right one to reuse next turn) | Routing the next user turn |
| The latest model response ID | OpenAI Responses chaining via |
| The raw | Provider-level diagnostics |
| Pending approvals and a resumable | Human-in-the-loop |
| Accumulated guardrail decisions | Logging why a run was blocked |
| Your app context plus SDK runtime metadata — including | Token tracking, DI, approvals |
Note that final_output is typed as Any (handoffs can change which agent — and which output_type — finishes), and in streaming mode it stays None until you finish consuming stream_events(). RunResultStreaming adds stream_events(), current_agent, is_complete, and cancel().
Token tracking and the Usage object
Every run automatically tracks token usage. You read it from result.context_wrapper.usage, which is a Usage object:
Three things matter for getting billing right
Usage is aggregated across the entire run — every model call, including those triggered by tool loops and handoffs. If a single Runner.run() makes three LLM calls, usage reflects all three combined.
reasoning_tokens is a subset of output_tokens, not an addition to it. On the OpenAI Responses API, reasoning tokens are billed as output tokens, so they're already counted inside output_tokens and surfaced separately under output_tokens_details.reasoning_tokens for visibility. Likewise, input_tokens_details.cached_tokens is a subset of input_tokens (cached prompt prefixes, usually billed at a discount). Don't add these subset fields back into the totals or you'll double-count.
Per-request breakdowns live in request_usage_entries. The aggregate hides individual calls; this list preserves each request's tokens, which is what you want for accurate per-call cost math or context-window monitoring.
You can also read usage live inside lifecycle hooks — context.usage is available in RunHooks / AgentHooks, so you can log per-agent token spend at on_agent_end:
With sessions, each Runner.run() returns usage for that run only — and because session history is re-fed as input on each turn, your input_tokens naturally grow turn over turn (a good reason to cap retrieval with SessionSettings(limit=...) or compaction). Finally, a production gotcha: with third-party adapters (LiteLLM, Any-LLM) some provider backends don't report usage unless you set ModelSettings(include_usage=True), especially on streamed Chat Completions backends. If total_tokens comes back as 0 on a non-OpenAI provider, that flag is the first thing to check.
Part 3 — Tools: how agents take action
The SDK supports five categories of tools.
1. Function tools
Decorate any Python function. The SDK extracts the JSON schema from the signature (via inspect + Pydantic) and the descriptions from the docstring (via griffe).
Function tools have a rich production surface
Timeouts
@function_tool(timeout=2.0) with timeout_behavior="error_as_result" (default, returns a model-visible message) or "raise_exception" (raises ToolTimeoutError).
Error handling
Pass failure_error_function=... to control the message the model sees on a crash, or None to re-raise.
Validation
Use Pydantic Field(..., ge=0, le=100) for constraints that end up in the JSON schema.
Rich returns
Return text, images (ToolOutputImage), or files (ToolOutputFileContent).
2. Hosted tools (OpenAI Responses)
Run alongside the model on OpenAI's servers: WebSearchTool, FileSearchTool (your vector stores), CodeInterpreterTool, ImageGenerationTool, HostedMCPTool, and ToolSearchTool (deferred tool loading for large tool surfaces).
Hosted tools require OpenAI Responses models. They are not available on Chat Completions or non-OpenAI backends.
3. Local / runtime execution tools
ComputerTool (GUI/browser automation against a Computer you implement), ShellTool (local or hosted-container execution), and ApplyPatchTool (apply diffs through your editor). These let an agent operate a real environment.
4. Agents as tools
Expose an agent as a callable tool without transferring control — the manager pattern. The orchestrator keeps the conversation and calls specialists like functions.
as_tool() supports structured input (parameters=), output extraction (custom_output_extractor=), streaming (on_stream=), conditional enabling (is_enabled=), and approval gates (needs_approval=).
5. MCP (Model Context Protocol)
MCP is "USB-C for AI tools." The SDK supports four integration styles:
Hosted MCP
HostedMCPTool — OpenAI's Responses API calls a public MCP server for you.
Streamable HTTP
MCPServerStreamableHttp — you run/manage the server.
HTTP+SSE
MCPServerSse — legacy transport.
stdio
MCPServerStdio — spawn a local subprocess.
MCP servers support tool filtering (static allow/block lists or dynamic callables), list_tools() caching, per-call _meta injection, approval policies, and prompts. Use MCPServerManager to connect many servers up front with reconnect handling.
Part 4 — Multi-agent design: handoffs vs. the manager pattern
There are two broadly applicable architectures, and the SDK supports both natively.
Handoffs
Handoffs are decentralized: a peer agent delegates the conversation to a specialist, which then takes over. Handoffs are represented to the model as tools (transfer_to_<agent_name>).
handoff() lets you customize the tool name and description, attach an on_handoff callback, require structured input_type metadata (e.g. a reason), filter the history the next agent sees with input_filter, and dynamically enable/disable via is_enabled. There's also an opt-in beta, nest_handoff_history, that collapses the prior transcript into a single summary message.
The manager pattern
The manager pattern (agents-as-tools, above) is centralized: the orchestrator never gives up control. Use handoffs when a specialist should own the rest of the conversation; use the manager pattern when you need to compose several specialists and synthesize their outputs.
For better routing, prepend RECOMMENDED_PROMPT_PREFIX (from agents.extensions.handoff_prompt) to your instructions.
Part 5 — Guardrails: cheap checks that protect expensive models
Guardrails run validations in parallel with or before your agent and can halt execution via a tripwire.
Input guardrails
Input guardrails run on the first agent's input. They support run_in_parallel=True (lowest latency, default) or run_in_parallel=False (blocking — the agent never starts if the tripwire trips, which saves tokens and avoids side effects).
Output guardrails
Output guardrails run on the final output of the last agent.
Tool guardrails
Tool guardrails wrap individual function tools — input guardrails before execution (can skip, replace output, or trip) and output guardrails after.
When a tripwire fires, the SDK raises InputGuardrailTripwireTriggered / OutputGuardrailTripwireTriggered, which you catch and turn into a graceful response. The classic pattern: a fast, cheap model (gpt-5-nano) screens input before your slow, expensive model ever runs.
Part 6 — Memory: sessions, context, and conversation state
There are four ways to carry state between turns, and choosing correctly is a production decision:
Strategy | Where state lives | Best for |
|---|---|---|
| Your app memory | Manual control, any provider |
| Your storage + SDK | Persistent, resumable chat |
| OpenAI Conversations API | Server-side, shared across services |
| OpenAI Responses API | Lightweight server-managed chaining |
The last two are OpenAI-managed and only apply to the Responses API. Pick one strategy per conversation — mixing client-managed history with server-managed state duplicates context.
Sessions
Sessions make the SDK retrieve history before each run and persist new items after. Backends include SQLiteSession (memory or file), AsyncSQLiteSession, RedisSession, SQLAlchemySession, MongoDBSession, DaprSession, OpenAIConversationsSession, AdvancedSQLiteSession (branching + analytics), EncryptedSession (transparent encryption + TTL), and OpenAIResponsesCompactionSession (auto-compaction for very long chats).
You can control retrieval with SessionSettings(limit=N), customize the history/new-input merge with RunConfig.session_input_callback, and implement your own backend by following the Session protocol.
Local context (dependency injection)
RunContextWrapper[T] carries a local object — your user ID, permissions, DB handles, logger — to every tool, hook, and guardrail. It is never sent to the LLM. It also exposes aggregated usage and, inside as_tool() runs, tool_input.
If you serialize
RunStatefor human-in-the-loop or durable jobs, the context travels with it — never put secrets in it.
Part 7 — Tracing and observability
Tracing is on by default. Every Runner.run() is wrapped in a trace, and each agent run, LLM generation, tool call, guardrail, and handoff becomes a span.
Production knobs
RunConfig(workflow_name=..., trace_id=..., group_id=..., trace_metadata=...) for naming and correlation.
RunConfig(trace_include_sensitive_data=False) (or OPENAI_AGENTS_TRACE_INCLUDE_SENSITIVE_DATA=0) to exclude inputs/outputs from trace payloads.
flush_traces() to force an immediate export in workers (Celery, RQ, FastAPI background tasks) where the process may not flush in time.
add_trace_processor() to also ship traces somewhere else, or set_trace_processors() to replace the default exporter.
The ecosystem is broad: Logfire, Langfuse, Braintrust, Datadog, W&B Weave, MLflow, Arize-Phoenix, LangSmith, Comet Opik, AgentOps, and many more support the SDK's tracing surface out of the box.
Under a Zero Data Retention (ZDR) policy, OpenAI tracing is unavailable — disable it or use a custom processor.
Part 8 — Models and providers: the part that future-proofs your project
This is the heart of the SDK's "no one-way doors" philosophy. There are two OpenAI model shapes:
OpenAI model types
OpenAIResponsesModel (recommended)
The Responses API. Required for hosted tools and tool search.
OpenAIChatCompletionsModel
The Chat Completions API. The lowest common denominator that almost every provider supports.
Four ways to point the SDK somewhere other than api.openai.com
Approach | Scope | When |
|---|---|---|
| Global default | One compatible endpoint for most/all agents |
| Per run | A custom provider for one run |
| Per agent | Different agents, different providers |
Third-party adapter (LiteLLM / Any-LLM) | Per model | Adapter-managed coverage/routing |
Plus the global switches: set_default_openai_api("chat_completions"), set_default_openai_key(...), and the OPENAI_BASE_URL / OPENAI_API_KEY environment variables.
The global client approach (the one you'll use most)
That's it. Your agents, tools, handoffs, and guardrails don't change at all.
Part 9 — The production scenario, end to end: OpenAI → vLLM/Triton → Together AI → Azure
This is the journey that motivates most adoptions, so let's make it concrete.
Stage 1 — MVP on OpenAI
You start fast. Set OPENAI_API_KEY, pick gpt-5.5, ship. Responses API, hosted tools, full tracing — everything works.
Stage 2 — Move the model on-prem to your own GPUs (vLLM)
Cost, latency, data residency, or compliance pushes you to host the model yourself. You serve, say, Llama or Qwen with vLLM, which exposes an OpenAI-compatible /v1 endpoint:
Now you point the SDK at it — and nothing else changes:
Stage 3 — Scale the serving layer with NVIDIA Triton (vLLM backend)
As traffic grows you move to NVIDIA Triton Inference Server with the vLLM backend for batching, multi-model hosting, and GPU scheduling. Triton can sit behind an OpenAI-compatible gateway, so from the SDK's perspective it's still just a base_url:
The agent code written in Stage 1 runs unchanged on infrastructure that didn't exist when you wrote it. That's the whole point.
Alternative — a managed open-model provider (Together AI)
Don't want to run GPUs at all? Together AI (and OpenRouter, Fireworks, Groq, etc.) expose OpenAI-compatible endpoints:
Azure OpenAI / Azure AI Foundry
Enterprises on Azure use a dedicated client, AsyncAzureOpenAI. The key difference: on Azure you reference a deployment name, not a model name.
Azure AI Foundry exposes a catalog of models (OpenAI and open models) the same way; mix in LiteLLM (azure/..., azure_ai/... model strings) when you need broader Foundry coverage or routing.
Mixing providers in one workflow
You can even run a small/fast model for triage and a large model for hard tasks — by setting model per agent, or by routing prefixes with MultiProvider (openai/..., litellm/..., any-llm/...). Just remember: feature support differs across providers (structured outputs, multimodal input, hosted tools), so validate the exact backend you ship.
Third-party adapters (LiteLLM / Any-LLM)
When the built-in paths aren't enough — you need adapter-managed coverage of 100+ providers or specific routing — install openai-agents[litellm] or openai-agents[any-llm] and use litellm/... or any-llm/... model names (or LitellmModel / AnyLLMModel directly). Note that some adapter backends need ModelSettings(include_usage=True) to report token usage.
Part 10 — The production playbook
Capabilities are easy; running them reliably is the real work. Here's the checklist that separates a demo from a service.
Retries — explicit and opt-in
The SDK does not retry general model calls unless you ask. Opt in with ModelSettings(retry=...):
Lead with provider_suggested() — it preserves provider vetoes and replay-safety approvals, which matters for stateful previous_response_id / conversation_id follow-ups.
Timeouts and tool failure isolation
Give every external-call tool a timeout, and decide whether a timeout should recover (error_as_result) or fail the run (raise_exception). Use failure_error_function so a flaky API returns a clean message to the model instead of crashing the turn.
Bounding the loop
Always set a max_turns in production and provide an error_handlers={"max_turns": ...} fallback so a runaway agent returns a controlled message instead of raising. The same mechanism handles "model_refusal".
Human-in-the-loop (HITL)
Mark sensitive tools with needs_approval=True (or a per-call callable). When the model calls one, the run pauses and surfaces result.interruptions. Serialize with result.to_state() → state.to_json(), store it in a queue or DB, then later RunState.from_json(...), state.approve(...) / state.reject(...), and resume with Runner.run(agent, state). This works across handoffs and nested as_tool() calls, and survives process restarts — the foundation for durable approvals.
Durable execution
For workflows that span long waits, retries, or restarts, the SDK integrates with Temporal, Dapr, Restate, and DBOS. These wrap the runner so progress survives failures — essential for HITL approvals that might sit for hours.
Security and data governance
OPENAI_AGENTS_DONT_LOG_MODEL_DATA=1 and OPENAI_AGENTS_DONT_LOG_TOOL_DATA=1 keep prompts/outputs out of logs (on by default).
trace_include_sensitive_data=False keeps them out of traces.
Don't put secrets in RunContextWrapper.context if you serialize RunState.
Use EncryptedSession for conversation history at rest, with TTL.
Tool guardrails can redact secrets (API keys, PII) before and after tool execution.
Concurrency and cost control
ModelSettings(parallel_tool_calls=...) controls whether the model may emit multiple tool calls; RunConfig(tool_execution=ToolExecutionConfig(max_function_tool_concurrency=N)) controls how many the SDK runs at once.
Use blocking input guardrails (run_in_parallel=False) to stop spend before the expensive model runs.
For long contexts, lean on truncation="auto", server-side context_management compaction, or OpenAIResponsesCompactionSession.
prompt_cache_retention="24h" keeps cached prefixes warm to cut cost and latency.
Observability discipline
Name every workflow (workflow_name), correlate multi-run conversations with group_id, attach trace_metadata, and flush_traces() in background workers. Wire a real backend (Langfuse/Datadog/Logfire) from day one — debugging agents blind is the most expensive mistake in this space.
Streaming and latency
Use Runner.run_streamed() and forward raw text deltas (ResponseTextDeltaEvent) for snappy UIs. For OpenAI Responses you can opt into websocket transport (set_default_openai_responses_transport("websocket")) and reuse connections across turns with responses_websocket_session().
Part 11 — A reference architecture you can copy
To make all of this concrete, the companion agent-boilerplate project packages the patterns above into a plug-and-play starter:
The single most important file is provider.py. It implements exactly the migration story from Part 9: one function reads MODEL_PROVIDER and configures the global client for OpenAI, Azure, or any compatible backend. Flip one environment variable and the same agents run on gpt-5.5, on a vLLM box, behind Triton, on Together AI, or on Azure AI Foundry — with zero code changes.
That property — the model is a deployment detail, not an architectural commitment — is why so many teams reach for the OpenAI Agents SDK first, and why they're still using it when their MVP turns into a platform.
Closing
The OpenAI Agents SDK earns its default-choice status by being honest about its scope: it orchestrates agents, and it refuses to lock you into a model, a provider, or an infrastructure.
Learn the five primitives:
agents
tools
handoffs
guardrails
sessions
Wire in tracing and provider configuration, then layer on the production playbook of retries, timeouts, HITL, and data governance.
Do that, and the gap between "impressive demo" and "service you can run at 3 a.m." mostly disappears.
Start on OpenAI.
Move to your own GPUs when it makes sense.
Keep your agents exactly as they are.
That's the promise — and, as this guide showed, it's one the SDK actually keeps.






