> ## Documentation Index
> Fetch the complete documentation index at: https://docs.decimal.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# OpenAI SDK

> Give an agent you build on the openai SDK, with your own Chat Completions or Responses API tool loop, its DecimalAI prompt and skills with agent_config(), one run per user message, and trace each run.

`agent_config()` reads an agent's prompt and skills from DecimalAI and hands them over per user message, as a run: `run.system` (the saved prompt, then the agent's skill list), `run.message` (the user turn) and `run.tools` (a `load_skill` tool), which you pass to your own Chat Completions or Responses API loop. Each run is one trace, filed under the DecimalAI agent. Nothing in the OpenAI SDK is patched, so only the requests you give these pieces to get DecimalAI's prompt and skills. `decimalai.openai` needs SDK 0.25 or later; through 0.24, code that called the OpenAI SDK itself used [`decimalai.agent_config()`](/sdk/python/frameworks#your-own-model-calls) (see [Moving from `decimalai.agent_config()`](#moving-from-decimalai-agent-config)).

Building on the OpenAI Agents SDK (`from agents import Agent`) instead? See [OpenAI Agents](/sdk/python/frameworks/openai-agents.md).

## Install

```bash theme={null}
pip install "decimalai[openai]"
```

The extra installs `openai` and `openinference-instrumentation-openai`, which `agent_config()` uses to trace its runs.

## Use

```python theme={null}
import json

from openai import OpenAI
from decimalai.openai import agent_config

client = OpenAI()
support = agent_config("support-agent", fallback="You are a support agent.")


def lookup_order(order_id: str) -> str:
    return f"Order {order_id} shipped yesterday."


TOOLS = [{"type": "function", "function": {
    "name": "lookup_order",
    "description": "Look up the status of an order.",
    "parameters": {"type": "object", "properties": {"order_id": {"type": "string"}},
                   "required": ["order_id"]},
}}]

question = "Where is order A1001?"
with support.run(question) as run:      # one user message = one run = one trace
    messages = [{"role": "system", "content": run.system}, run.message]
    while True:
        reply = client.chat.completions.create(
            model="gpt-5.4-mini",
            messages=messages,
            tools=[*TOOLS, *run.tools],  # your tools, then load_skill
        ).choices[0].message
        messages.append(reply)
        if not reply.tool_calls:
            break
        for call in reply.tool_calls:
            # load_skill is answered by the run; your own tools by you.
            messages.append(run.tool_result(call) or {
                "role": "tool", "tool_call_id": call.id,
                "content": lookup_order(**json.loads(call.function.arguments)),
            })
print(reply.content)
```

* **`agent_config("support-agent")`** reads the agent's saved prompt when it is called. It initializes the SDK from `DECIMAL_API_KEY` when `decimalai.init()` has not been called, and a name your workspace has no agent by is created, so a misspelled name makes a new agent rather than an error. `fallback=` is what runs when the agent has no saved prompt yet, when no DecimalAI key is set, or when DecimalAI cannot be reached at startup; with a key and no `fallback=`, an unreachable DecimalAI raises instead of starting an agent with no prompt. With no key at all, the agent runs [without a DecimalAI key](/sdk/python/frameworks.md#without-a-decimalai-key): the skills your project lists in `.decimal/skills.toml`, with nothing sent.
* **`support.run(question)`** routes the agent's skill list on `question` and opens the run's trace. It calls no model: make the model calls inside the `with` block.
* **`run.system`** is the system message's text: the saved prompt, then the agent's skill list with one sentence telling the model to load a skill before using it.
* **`run.message`** is the user turn, `{"role": "user", "content": question}`. Send it instead of your own user dict: in a [chat](#chats) it can carry more.
* **`run.tools`** holds the `load_skill` tool in Chat Completions' shape, limited to the agent's skills. The model calls it with a skill's name and gets the skill's full text back, which counts as *delivered* and *activated* on the [usage ladder](/guides/skills.md#the-usage-ladder). The list is empty when the run has no skills to offer.
* **`run.tool_result(call)`** answers a `load_skill` call with its `tool` message, and returns `None` for any other tool, so `or` hands the call to yours. Each load names the run's routing decision, and the platform decides what it serves: a skill your org owns, has installed or has assigned to the agent loads whether or not this run's list showed it, while one reachable only through the public registry loads only when this run was offered it. A refusal (an unknown name, an outage) comes back as a sentence the model can read. `run.load_skill(name)` returns the same text alone, for a dispatcher of your own.
* **No `decimalai.init()`.** With a key, `agent_config()` initializes the SDK from `DECIMAL_API_KEY` and turns its tracing on itself. `init()` refuses to start without a key, so a file that calls it cannot run keyless.

`decimalai init <agent> --framework openai` writes this loop as a runnable file, with an example tool.

### The Responses API

The same run, on `client.responses.create()`: the system text goes in `instructions`, the user turn in `input`, and the tool from `run.response_tools` (the same `load_skill`, in the Responses API's flat shape, strict). `run.tool_result(call)` answers a `function_call` item with its `function_call_output`:

```python theme={null}
with support.run(question) as run:
    response = client.responses.create(
        model="gpt-5.4-mini", instructions=run.system,
        input=[run.message], tools=[*TOOLS, *run.response_tools],
    )
    while calls := [item for item in response.output if item.type == "function_call"]:
        response = client.responses.create(
            model="gpt-5.4-mini", instructions=run.system,   # send them again: see below
            previous_response_id=response.id, tools=[*TOOLS, *run.response_tools],
            input=[run.tool_result(call) or call_my_tool(call) for call in calls],
        )
print(response.output_text)
```

A request with `previous_response_id` does not carry the earlier request's `instructions` over, so send `run.system` with every request.

### Async, and other clients

* **`AsyncOpenAI`**: open the run with `async with support.run(question) as run:`, and answer each call with `await run.atool_result(call)` (`await run.aload_skill(name)` for the text alone), which keep the load off the event loop.
* **OpenAI-compatible endpoints**: the tools are plain dicts and `run.tool_result()` reads a call as the SDK returns it or as a dict, so an `OpenAI(base_url=...)` client for Azure OpenAI, a local server or another provider's OpenAI-compatible endpoint takes the same run, and its calls are traced.
* **LiteLLM**: `litellm.completion()` takes `run.system`, `run.message` and `run.tools` the same way, and `run.tool_result()` answers its tool calls. Calls made through a library other than `openai` are not traced; the run's record is.

## Chats

A chat keeps one conversation, and a conversation's system text and tool stay what its first message got, so every request in the chat starts with the same prefix (which OpenAI's prompt cache reuses). A newly saved prompt or a newly assigned skill reaches the next conversation, not an open one.

```python theme={null}
chat = support.conversation(variables={"customer": {"name": "Sam"}})   # one per chat
history = []                                                     # the chat's messages, kept with it

with chat.run(question) as run:
    history.append(run.message)                 # the question (+ skills new to this chat)
    reply = client.chat.completions.create(
        model="gpt-5.4-mini", tools=[*TOOLS, *run.tools],
        messages=[{"role": "system", "content": run.system}, *history],
    ).choices[0].message
    history.append(reply)                       # ...and the tool results, as in Use
saved = chat.state                              # JSON: support.conversation(state=saved) resumes it
```

* **`run.message`** is the user turn to send: the question, plus, for an agent whose skill list is cut to fit, the skills routed for this message that the chat has not shown yet, after the question. Send it instead of your own user dict, or the model is not shown what this message's routing picked.
* **`chat.state`** is the conversation as JSON-ready data. A server that keeps no chat in memory stores it beside the chat's history, and `support.conversation(state=saved)` resumes it in another process, with the same system text and tool. A state for another agent, or `state=` together with `variables=`, raises `ValueError`.
* **Variables** are the conversation's: they are filled once, at its first message. Put facts that change per message in the user message.

## Change the prompt without redeploying

`agent_config()` keeps a local copy of the prompt and re-reads it in the background at most once a minute, so a version saved in the dashboard's Prompt tab reaches the next run within a minute, with no redeploy (an open [conversation](#chats) keeps its first message's). Pinning an older version there, or serving the latest again, works the same way.

A saved prompt can hold variables, written `{{name}}`, filled from `variables=` as attributes or dict keys (`{{customer.name}}` reads `variables["customer"]["name"]`):

```python theme={null}
with support.run(question, variables={"customer": {"name": "Sam"}}) as run:
    ...
```

A variable with no value stops the run before any model call, with a `MissingPromptVariable` error (`decimalai.MissingPromptVariable`) that names it. DecimalAI fills only the prompt, never a skill's text, and the trace keeps the placeholder names, not their values.

## How each run's skills are picked

Each run's skill list is routed on that run's own message.

* **The first run** is routed before it opens.
* **Later runs** show the local copy of the list, and their own routing decision is recorded in the background, so no run waits on it. Closing the run does not wait either: the trace is sent once the decision lands.
* **An agent with more skills than the list holds** is routed before every run, so the list is picked for the message.
* **A later message of a conversation** shows the conversation's list. When the list was cut to fit, the message is still routed on its own text, and the skills it picks that the chat has not shown go into `run.message`.

A run makes one routing decision however many tool calls it takes, and every request in it sees the same list. When the agent's whole list fits, `load_skill` takes only the names on it, and another name is answered with the list and loads nothing; when the list was cut to fit, it takes any name, and the platform serves a skill of the agent's that the cap left off.

## When an agent's skills are switched off

* **The tool's kill switch.** With `DECIMALAI_LOAD_SKILL_TOOL=0` or `decimalai.init(load_skill_tool=False)`, every run is routed and the text of the skills picked for it goes into `run.system` instead (`inject_skill_body`, which defaults to on once the tool is off), and `run.tools` is empty. With `inject_skill_body=False` as well, the agent gets no skills, and one warning says so.
* **`decimalai.init(enabled=False)`** after `agent_config()` stops routing and skills for that config; the prompt it read keeps running, and an open conversation keeps its system text. An `agent_config()` called while it is off asks DecimalAI nothing: the agent runs on its `fallback=` text, and without one `agent_config()` raises `RuntimeError`.
* **`on_unavailable="fail"`.** By default (`"continue"`) a run whose skill list never came back answers without skills, and its trace says so. With `agent_config(..., on_unavailable="fail")` opening the run raises instead.

Loading a skill costs a model request: the model asks for the skill in one request and uses it in the next. An outage during a run is not an error: `load_skill` answers with a sentence the model can read.

## Tracing

`agent_config()` turns on OpenAI tracing (OpenInference's instrumentor, which wraps the SDK's classes for the process) on the same pipeline as `decimalai.init(openai=True)`, and sends only its runs:

* **Each run is one trace**, filed under `support.name`, with its model calls (Chat Completions and Responses API alike), the prompt version, the variable names it filled, and the skills offered, loaded and delivered. A conversation's runs share its id as the trace's `session_id`. A run opened inside your own span (a web server's request span) is a trace of its own, linked to that span.
* **Nothing outside a run is sent.** An OpenAI call made outside one, in any client of the process, is held back, and so are the app's own spans. `decimalai.init(openai=True)` sends every OpenAI call; `decimalai.init(otel=True)`, the app's spans.
* **Set up tracing first.** Your own OpenTelemetry tracer provider, and `decimalai.init(otel=True)`, go before the first `agent_config()`: OpenTelemetry sets the global provider once.
* **Another tool's tracing.** When the instrumentor was turned on outside DecimalAI (Phoenix, Langfuse, your own setup), DecimalAI does not turn it on again; its runs are traced as long as that tracing sends its spans to the global tracer provider.
* **Reading each trace.** `agent_config(..., on_trace=fn)` calls `fn` once per run with a copy of the finished trace before it is sent. A run made inside `decimalai.start_trace()` is linked to it.

A run whose model calls were made after its `with` block logs one warning that no OpenAI request was traced inside it.

## What gets captured

* One trace per run: each model request with its input and output, and each `load_skill` result as a tool message in the next request's input
* The prompt version, the variable names it filled, any skill outage, and the skills offered, loaded and delivered, each delivery with the exact version the model received
* An auto-detected manifest: the model and each tool, by name. Not the system text: a run's prompt is the saved one, whose version each trace names.
* Token counts, from each response's usage

> [!TIP]
>
> **Checkpoint:** run the agent from [Use](#use) once with a question that matches one of your skills, then `print(support)`. It names the prompt version the agent ran and the skills on its list, for example `OpenAISDKAgentConfig(name='support-agent', prompt_version=3, skills=['refund-policy'])`, and the run appears on the [Traces page](https://app.decimal.ai/traces) under that name. No trace? Check that the model calls were made inside the `with` block. An empty skill list means the router had none to offer: check that your workspace has the skill installed or forked, and that `agent_config()` names the agent it is assigned to (see the silent no-ops list in the [Quickstart](/quickstart.md#if-something-looks-wrong-the-six-silent-no-ops)).

## Moving from `decimalai.agent_config()`

Through 0.24, OpenAI code used the top-level `decimalai.agent_config()`: it wrapped `run.tool_spec` in OpenAI's tool shape itself, answered `load_skill` with `run.load_skill(name)`, and needed `decimalai.init(openai=True)`, `agent_run()` and the instrumentor package for a run to reach a trace. That still works. `decimalai.openai` does the same in OpenAI's shapes, and traces each run with no other line:

```python theme={null}
# 0.24
import decimalai
from decimalai.providers import agent_run

decimalai.init(openai=True)
support = decimalai.agent_config("support-agent")
with agent_run(support.name):
    run = support.run(question)
    tools = [*my_tools, {"type": "function", "function": run.tool_spec}]
    ...   # each load_skill call: {"role": "tool", ..., "content": run.load_skill(name)}

# 0.25
from decimalai.openai import agent_config

support = agent_config("support-agent")
with support.run(question) as run:
    tools = [*my_tools, *run.tools]
    ...   # each call: run.tool_result(call) or call_my_tool(call)
```

## What's next

> **[Skills](/sdk/python/skills.md)**
>
> Skill list vs body delivery, the load\_skill tool, and the usage ladder.

> **[OpenAI Agents](/sdk/python/frameworks/openai-agents.md)**
>
> The same prompt and skills for an agent built on the OpenAI Agents SDK.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.