# `LangChain.ChatModels.ChatOpenAIResponses`
[🔗](https://github.com/brainlid/langchain/blob/v0.13.0/lib/chat_models/chat_open_ai_responses.ex#L1)

Represents the OpenAI Responses API

Parses and validates inputs for making requests to the OpenAI Responses API.

Converts responses into more specialized `LangChain` data structures.

## ContentPart Types

OpenAI's Responses API supports several types of content parts that can be combined in a single message:

### Text Content
Basic text content is the default and most common type:

    Message.new_user!("Hello, how are you?")

### Image Content
OpenAI supports both base64-encoded images and image URLs:

    # Using a base64 encoded image
    Message.new_user!([
      ContentPart.text!("What's in this image?"),
      ContentPart.image!("base64_encoded_image_data", media: :jpg)
    ])

    # Using an image URL
    Message.new_user!([
      ContentPart.text!("Describe this image:"),
      ContentPart.image_url!("https://example.com/image.jpg")
    ])

    # Using a file ID (after uploading to OpenAI)
    Message.new_user!([
      ContentPart.text!("Describe this image:"),
      ContentPart.image!("file-1234", type: :file_id)
    ])

For images, you can specify the detail level which affects token usage:
- `detail: "low"` - Lower resolution, fewer tokens
- `detail: "high"` - Higher resolution, more tokens
- `detail: "auto"` - Let the model decide

### File Content
OpenAI supports both base64-encoded files and file IDs:

    # Using a base64 encoded file
    Message.new_user!([
      ContentPart.text!("Process this file:"),
      ContentPart.file!("base64_encoded_file_data",
        type: :base64,
        filename: "document.pdf"
      )
    ])

    # Using a file ID (after uploading to OpenAI)
    Message.new_user!([
      ContentPart.text!("Process this file:"),
      ContentPart.file!("file-1234", type: :file_id)
    ])

## Callbacks

See the set of available callbacks: `LangChain.Chains.ChainCallbacks`

### Rate Limit API Response Headers

OpenAI returns rate limit information in the response headers. Those can be
accessed using the LLM callback `on_llm_ratelimit_info` like this:

    handlers = %{
      on_llm_ratelimit_info: fn headers ->
        IO.inspect(headers)
      end
    }

    {:ok, chat} = ChatOpenAIResponses.new(%{callbacks: [handlers]})

Handlers assigned to the model are fired by the model itself and receive only
the event's argument. Handlers assigned to an `LangChain.Chains.LLMChain`
receive the chain as an additional first argument.

When a request is received, something similar to the following will be output
to the console.

    %{
      "x-ratelimit-limit-requests" => ["5000"],
      "x-ratelimit-limit-tokens" => ["160000"],
      "x-ratelimit-remaining-requests" => ["4999"],
      "x-ratelimit-remaining-tokens" => ["159973"],
      "x-ratelimit-reset-requests" => ["12ms"],
      "x-ratelimit-reset-tokens" => ["10ms"],
      "x-request-id" => ["req_1234"]
    }

### Token Usage

OpenAI returns token usage information as part of the response body. The
`LangChain.TokenUsage` is added to the `metadata` of the `LangChain.Message`
and `LangChain.MessageDelta` structs that are processed under the `:usage`
key.

The OpenAI documentation instructs to provide the `stream_options` with the
`include_usage: true` for the information to be provided.

The `TokenUsage` data is accumulated for `MessageDelta` structs and the final usage information will be on the `LangChain.Message`.

NOTE: Of special note is that the `TokenUsage` information is returned once
for all "choices" in the response. The `LangChain.TokenUsage` data is added to
each message, but if your usage requests multiple choices, you will see the
same usage information for each choice but it is duplicated and only one
response is meaningful.

## Reasoning Continuity

Reasoning models such as `gpt-5` and the o-series return a `reasoning` output
item alongside their text and tool calls. Handing that item back on the next
request lets the model resume the reasoning it already did instead of
re-deriving it, which matters most inside a tool loop where every tool result
is another round trip.

The item is only replayable when it carries its `encrypted_content`, and the
API only returns that when the request asks for it:

    ChatOpenAIResponses.new!(%{
      model: "gpt-5",
      include: ["reasoning.encrypted_content"],
      reasoning: %{effort: :high}
    })

With that set, reasoning items are captured off the response and sent back as
their own `input` items on subsequent requests. Nothing else is required. A
reasoning item that arrived without `encrypted_content` is omitted rather than
sent as a stub, so leaving `:include` alone keeps the previous behavior.

This is the stateless path, and it works whether or not responses are stored.
`:previous_response_id` covers the stored case instead, and is unavailable to
a Zero Data Retention organization, which the API treats as stateless by
definition.

## Native Tools (Web Search)

Open AI's Responses API also supports built-in tools. Among those, we support Web Search currently.

### Example
To optionally permit the model to use web search:

    native_web_tool = NativeTool.new!(%{name: "web_search_preview", configuration: %{}})

    %{llm: ChatOpenAIResponses.new!(%{model: "gpt-4o"})}
    |> LLMChain.new!()
    |> LLMChain.add_message(Message.new_user!("Can you tell me something that happened today in Texas?"))
    |> LLMChain.add_tools(web_tool)
    |> LLMChain.run()

You may provide additional configuration per the OpenAI documentation:

    web_config = %{
      search_context_size: "medium",
      user_location: %{
        type: "approximate",
        city: "Humble",
        country: "US",
        region: "Texas",
        timezone: "America/Chicago"
      }
    }
    native_web_tool = NativeTool.new!(%{name: "web_search_preview", configuration: web_config)

You may reference a prior web_search_call in subsequent runs as:

    Message.new_assistant!([
      ContentPart.new!(%{
        type: :unsupported,
        options: [
          id: "ws_123456789", # ID as provided from Open AI
          status: "completed",
          type: "web_search_call"
        ]
      }),
      ContentPart.text!("The Astros won today 5-4...")
    ])

Note: Not all Open AI models support `web_search_preview`. OpenAI will return an error if you request web_search_preview for when using a model that doesn't support it.

## Tool Choice

OpenAI's ChatGPT API supports forcing a tool to be used.
- https://platform.openai.com/docs/api-reference/chat/create#chat-create-tool_choice

This is supported through the `tool_choice` options. It takes a plain Elixir
map to provide the configuration.

By default, the LLM will choose a tool call if a tool is available and it
determines it is needed. That's the "auto" mode.

### Example
For the LLM's response to make a tool call of the "get_weather" function.

    ChatOpenAI.new(%{
      model: "...",
      tool_choice: %{"type" => "function", "function" => %{"name" => "get_weather"}}
    })

...or to force a native tool (such as web search):

    ChatOpenAI.new(%{
      model: "...",
      tool_choice: "web_search_preview"
    })

## Verbosity

The `verbosity` option controls the length of the model's response. Accepted
values are `"low"`, `"medium"`, and `"high"`. When omitted, the API uses its
default behavior.

This is sent as part of the `text` parameter in the Responses API and can be
combined with JSON response formats.

Only supported for gpt-5 or newer models

### Example

    ChatOpenAIResponses.new!(%{model: "gpt-5", verbosity: "low"})

## Context Management

The `context_management` option configures server-side context compaction for
long-running conversations. When the rendered token count crosses the
threshold, the server compacts the context before continuing inference.

For example, compact the context after it reaches 200,000 tokens:

    ChatOpenAIResponses.new!(%{
      context_management: [%{type: "compaction", compact_threshold: 200_000}]
    })

`compact_threshold` is optional. When omitted, the server picks a threshold:

    ChatOpenAIResponses.new!(%{context_management: [%{type: "compaction"}]})

Only the `"compaction"` type is supported today, but entries are passed
through to the API as given, so new types and options work without a library
change.

## WebSocket Transport

Instead of HTTP, requests can be sent over a persistent WebSocket connection
for lower latency. Use `connect_websocket!/1` to open a connection and
`disconnect_websocket!/1` to close it:

    model =
      ChatOpenAIResponses.new!(%{model: "gpt-4o"})
      |> ChatOpenAIResponses.connect_websocket!()

    {:ok, chain} =
      %{llm: model}
      |> LLMChain.new!()
      |> LLMChain.add_message(Message.new_user!("Hello"))
      |> LLMChain.run()

    ChatOpenAIResponses.disconnect_websocket!(model)

The WebSocket connection is reused across multiple LLM calls within the same
chain run (e.g. multi-turn tool calling with `:while_needs_response`).

### Lifecycle Management

**The application is responsible for managing the WebSocket lifecycle.**
`connect_websocket!/1` starts a `LangChain.WebSocket` GenServer via
`start_link/1`, linking it to the calling process. The PID is stored in
the model struct's `:websocket` field. There is no supervisor, automatic
reconnection, or health monitoring built in.

This means:

- If the calling process exits, the WebSocket is terminated (process link).
- The WebSocket PID cannot be serialized. If the model struct is persisted
  to a database and restored later, the `:websocket` field will be stale.
- The server may close idle connections at any time. There is no automatic
  reconnection.
- There is no retry logic for WebSocket failures (unlike the HTTP transport).

**The WebSocket transport is best suited for short-lived, synchronous
sessions** where you control the full lifecycle. It is not currently safe
for long-lived agent processes, human-in-the-loop workflows with
interruptions, or any scenario where the model struct is serialized and
restored across process boundaries.

For long-running or interruptible workloads, use the default HTTP transport.

### Known Limitation: temperature and top_p

The `:temperature` and `:top_p` parameters are currently excluded from
WebSocket payloads due to an
[OpenAI bug](https://community.openai.com/t/1375536) that silently closes
the connection when these are sent as decimals. A `Logger.warning` is
emitted when these values are dropped. This workaround will be removed
once OpenAI resolves the issue.

## Connection Retry Behavior

The `retry_count` option controls how many times a request is retried when
a pooled HTTP connection turns out to be stale (server closed it between
requests). This is a transport-level issue where retrying with a fresh
connection is the correct response.

**Only closed-connection errors are retried.** Timeouts, rate limits (429),
overloaded (529), authentication errors, and invalid requests all return
immediately -- they are not problems that a simple retry will fix.

| `retry_count` | Total HTTP requests |
|---|---|
| `0` | 1 (no retries) |
| `1` | 2 (1 initial + 1 retry) |
| `2` (default) | 3 (1 initial + 2 retries) |

Req's built-in HTTP retry is disabled to prevent the two retry layers from
compounding. See [GitHub issue #503](https://github.com/brainlid/langchain/issues/503).

When running LLM calls from a background job queue (e.g., Oban) that has its
own retry logic, set `retry_count: 0` so there are no hidden retries:

    ChatOpenAIResponses.new!(%{model: "...", retry_count: 0})

# `t`
[🔗](https://github.com/brainlid/langchain/blob/v0.13.0/lib/chat_models/chat_open_ai_responses.ex#L408)

```elixir
@type t() :: %LangChain.ChatModels.ChatOpenAIResponses{
  api_key: term(),
  callbacks: term(),
  context_management: term(),
  endpoint: term(),
  include: term(),
  json_response: term(),
  json_schema: term(),
  json_schema_name: term(),
  max_output_tokens: term(),
  model: term(),
  previous_response_id: term(),
  reasoning: term(),
  receive_timeout: term(),
  req_config: term(),
  retry_count: term(),
  store: term(),
  stream: term(),
  temperature: term(),
  tool_choice: term(),
  top_p: term(),
  truncation: term(),
  user: term(),
  verbose_api: term(),
  verbosity: term(),
  websocket: term()
}
```

# `connect_websocket`
[🔗](https://github.com/brainlid/langchain/blob/v0.13.0/lib/chat_models/chat_open_ai_responses.ex#L496)

```elixir
@spec connect_websocket(t()) :: {:ok, t()} | {:error, String.t()}
```

Open a `LangChain.WebSocket` connection using the model's endpoint and API key.

Returns `{:ok, model}` with the `:websocket` field set to the WebSocket PID,
or `{:error, reason}` on failure.

Requires the optional `mint_web_socket` dependency.

## Example

    {:ok, model} = ChatOpenAIResponses.connect_websocket(model)

# `connect_websocket!`
[🔗](https://github.com/brainlid/langchain/blob/v0.13.0/lib/chat_models/chat_open_ai_responses.ex#L542)

```elixir
@spec connect_websocket!(t()) :: t() | no_return()
```

Like `connect_websocket/1` but raises on failure.

## Example

    model =
      ChatOpenAIResponses.new!(%{model: "gpt-4o"})
      |> ChatOpenAIResponses.connect_websocket!()

    # ... use model in chains ...

    ChatOpenAIResponses.disconnect_websocket!(model)

# `content_part_for_api`
[🔗](https://github.com/brainlid/langchain/blob/v0.13.0/lib/chat_models/chat_open_ai_responses.ex#L1016)

Convert a ContentPart to the expected map of data for the OpenAI API.

See `content_parts_for_api/3` for how `role` affects the conversion.

# `content_parts_for_api`
[🔗](https://github.com/brainlid/langchain/blob/v0.13.0/lib/chat_models/chat_open_ai_responses.ex#L1005)

Convert a list of ContentParts to the expected map of data for the OpenAI API.

The `role` of the message being converted determines which content part types
the API accepts. Input messages (`:system`, `:developer`, and `:user`) use the
`input_*` part types. Assistant messages are "output" messages and only
support `output_text`; unsupported parts are omitted.

# `decode_stream`
[🔗](https://github.com/brainlid/langchain/blob/v0.13.0/lib/chat_models/chat_open_ai_responses.ex#L1447)

```elixir
@spec decode_stream({String.t(), String.t()}) :: {[map()], String.t()}
```

# `disconnect_websocket!`
[🔗](https://github.com/brainlid/langchain/blob/v0.13.0/lib/chat_models/chat_open_ai_responses.ex#L562)

```elixir
@spec disconnect_websocket!(t()) :: t()
```

Close the WebSocket connection associated with this model.

Returns the model with `:websocket` set to `nil`.
Safe to call even if the WebSocket is already closed.

# `do_api_request`
[🔗](https://github.com/brainlid/langchain/blob/v0.13.0/lib/chat_models/chat_open_ai_responses.ex#L1196)

```elixir
@spec do_api_request(
  t(),
  [LangChain.Message.t()],
  LangChain.ChatModels.ChatModel.tools(),
  integer() | nil
) :: list() | struct() | {:error, LangChain.LangChainError.t()}
```

# `for_api`
[🔗](https://github.com/brainlid/langchain/blob/v0.13.0/lib/chat_models/chat_open_ai_responses.ex#L795)

```elixir
@spec for_api(
  struct(),
  LangChain.Message.t()
  | LangChain.PromptTemplate.t()
  | LangChain.Message.ToolCall.t()
  | LangChain.Message.ToolResult.t()
  | LangChain.Message.ContentPart.t()
  | LangChain.Function.t()
  | LangChain.NativeTool.t()
) :: %{required(String.t()) =&gt; any()} | [%{required(String.t()) =&gt; any()}]
```

# `for_api`
[🔗](https://github.com/brainlid/langchain/blob/v0.13.0/lib/chat_models/chat_open_ai_responses.ex#L631)

```elixir
@spec for_api(
  t() | LangChain.Message.t() | LangChain.Function.t(),
  message :: [map()],
  LangChain.ChatModels.ChatModel.tools()
) :: %{required(atom()) =&gt; any()}
```

Return the params formatted for an API request.

# `native_tool_call_for_api`
[🔗](https://github.com/brainlid/langchain/blob/v0.13.0/lib/chat_models/chat_open_ai_responses.ex#L911)

```elixir
@spec native_tool_call_for_api(any(), any()) ::
  nil | %{id: any(), status: any(), type: &lt;&lt;_::120&gt;&gt;}
```

# `native_tool_calls_for_api`
[🔗](https://github.com/brainlid/langchain/blob/v0.13.0/lib/chat_models/chat_open_ai_responses.ex#L903)

# `new`
[🔗](https://github.com/brainlid/langchain/blob/v0.13.0/lib/chat_models/chat_open_ai_responses.ex#L459)

```elixir
@spec new(attrs :: map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()}
```

Setup a ChatOpenAI client configuration.

# `new!`
[🔗](https://github.com/brainlid/langchain/blob/v0.13.0/lib/chat_models/chat_open_ai_responses.ex#L471)

```elixir
@spec new!(attrs :: map()) :: t() | no_return()
```

Setup a ChatOpenAI client configuration and return it or raise an error if invalid.

# `reasoning_item_for_api`
[🔗](https://github.com/brainlid/langchain/blob/v0.13.0/lib/chat_models/chat_open_ai_responses.ex#L968)

Convert a content part holding a reasoning item back into the reasoning entry
the API accepts in `input`. Returns `nil` for any other content part.

A reasoning item is only accepted back when it carries its
`encrypted_content`, which the API returns when the request asked for it with
`include: ["reasoning.encrypted_content"]`. Without that payload there is
nothing to hand back, so the item is omitted rather than sent as a stub.

Both shapes a reasoning item arrives as are recognized: the `:unsupported`
part built from a non-streamed response, and the `:thinking` part the
streaming events build.

# `restore_from_map`
[🔗](https://github.com/brainlid/langchain/blob/v0.13.0/lib/chat_models/chat_open_ai_responses.ex#L2291)

Restores the model from the config.

# `retry_on_fallback?`
[🔗](https://github.com/brainlid/langchain/blob/v0.13.0/lib/chat_models/chat_open_ai_responses.ex#L2304)

```elixir
@spec retry_on_fallback?(LangChain.LangChainError.t()) :: boolean()
```

Determine if an error should be retried with a fallback model.
Aligns with other providers.

# `serialize_config`
[🔗](https://github.com/brainlid/langchain/blob/v0.13.0/lib/chat_models/chat_open_ai_responses.ex#L2263)

```elixir
@spec serialize_config(t()) :: %{required(String.t()) =&gt; any()}
```

Generate a config map that can later restore the model's configuration.

# `stand_alone_items_for_api`
[🔗](https://github.com/brainlid/langchain/blob/v0.13.0/lib/chat_models/chat_open_ai_responses.ex#L939)

Convert the content parts of an assistant message into the stand-alone items
the API expects in `input`, preserving their original order.

Some content parts do not belong to the assistant message item at all. A
reasoning item and a native tool call are each their own entry in the
`output` array the API returned, and each must go back as its own entry in
`input`. Order matters: the API expects a reasoning item to precede the
message or function call it produced.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
