Current section
Files
Jump to
Current section
Files
lib/openai_responses.ex
defmodule OpenAI.Responses do
@moduledoc """
Client for the OpenAI Responses API.
This module provides functions to interact with OpenAI's Responses API,
allowing you to create, retrieve, and manage AI-generated responses.
## Examples
# Create a simple text response
{:ok, response} = OpenAI.Responses.create("gpt-4o", "Write a haiku about programming")
# Extract the text from the response
text = OpenAI.Responses.Helpers.output_text(response)
# Create a response with tools and options
{:ok, response} = OpenAI.Responses.create("gpt-4o", "What's the weather like in Paris?",
tools: [%{type: "web_search_preview"}],
temperature: 0.7
)
# Stream a response
stream = OpenAI.Responses.create_stream("gpt-4o", "Tell me a story")
Enum.each(stream, fn event -> IO.inspect(event) end)
"""
alias OpenAI.Responses.Client
alias OpenAI.Responses.Types
@doc ~S"""
Creates a new response with the specified model and input.
## Parameters
* `model` - The model ID to use (e.g., "gpt-4o")
* `input` - The text prompt or structured input message
* `opts` - Optional parameters for the request
* `:tools` - List of tools to make available to the model
* `:instructions` - System instructions for the model
* `:temperature` - Sampling temperature (0.0 to 2.0)
* `:max_output_tokens` - Maximum number of tokens to generate
* `:stream` - Whether to stream the response
* `:previous_response_id` - ID of a previous response for continuation
* All other parameters supported by the API
## Returns
* `{:ok, response}` - On success, returns the response
* `{:error, error}` - On failure
"""
@spec create(String.t(), String.t() | map() | list(), keyword()) ::
{:ok, map()} | {:error, any()}
def create(model, input, opts \\ []) do
client = opts[:client] || Client.new(opts)
payload = prepare_create_payload(model, input, opts)
case Client.request(client, :post, "/responses", payload) do
{:ok, response} -> {:ok, Types.response(response)}
error -> error
end
end
@doc ~S"""
Creates a streaming response with the specified model and input.
This function is being maintained for backward compatibility.
New code should use `stream/3` instead.
## Returns
* A stream of events representing the model's response
"""
@spec create_stream(String.t(), String.t() | map() | list(), keyword()) :: Enumerable.t()
def create_stream(model, input, opts \\ []) do
stream(model, input, opts)
end
@doc ~S"""
Creates a streaming response and returns a proper Enumerable stream of events.
This function returns a stream that yields individual events as they arrive from the API,
making it suitable for real-time processing of responses.
## Parameters
* `model` - The model ID to use (e.g., "gpt-4o")
* `input` - The text prompt or structured input message
* `opts` - Optional parameters for the request (same as `create/3`)
## Examples
# Print each event as it arrives
stream = OpenAI.Responses.stream("gpt-4o", "Tell me a story")
Enum.each(stream, &IO.inspect/1)
# Process text deltas in real-time
stream = OpenAI.Responses.stream("gpt-4o", "Tell me a story")
text_stream = OpenAI.Responses.Stream.text_deltas(stream)
# This preserves streaming behavior (one chunk at a time)
text_stream
|> Stream.each(fn delta ->
IO.write(delta)
end)
|> Stream.run()
## Returns
* An Enumerable stream that yields events as they arrive
"""
@spec stream(String.t(), String.t() | map() | list(), keyword()) :: Enumerable.t()
def stream(model, input, opts \\ []) do
client = opts[:client] || Client.new(opts)
payload = prepare_create_payload(model, input, Keyword.put(opts, :stream, true))
Client.stream(client, "/responses", payload)
end
@doc ~S"""
Retrieves a specific response by ID.
## Parameters
* `response_id` - The ID of the response to retrieve
* `opts` - Optional parameters for the request
* `:include` - Additional data to include in the response
## Returns
* `{:ok, response}` - On success, returns the response
* `{:error, error}` - On failure
"""
@spec get(String.t(), keyword()) :: {:ok, map()} | {:error, any()}
def get(response_id, opts \\ []) do
client = opts[:client] || Client.new(opts)
query = if opts[:include], do: %{include: opts[:include]}, else: %{}
case Client.request(client, :get, "/responses/#{response_id}", nil, query) do
{:ok, response} -> {:ok, Types.response(response)}
error -> error
end
end
@doc ~S"""
Deletes a specific response by ID.
## Parameters
* `response_id` - The ID of the response to delete
* `opts` - Optional parameters for the request
## Returns
* `{:ok, result}` - On success, returns deletion confirmation
* `{:error, error}` - On failure
"""
@spec delete(String.t(), keyword()) :: {:ok, map()} | {:error, any()}
def delete(response_id, opts \\ []) do
client = opts[:client] || Client.new(opts)
Client.request(client, :delete, "/responses/#{response_id}")
end
@doc ~S"""
Lists input items for a specific response.
## Parameters
* `response_id` - The ID of the response
* `opts` - Optional parameters for the request
* `:before` - List input items before this ID
* `:after` - List input items after this ID
* `:limit` - Number of objects to return (1-100)
* `:order` - Sort order ("asc" or "desc")
## Returns
* `{:ok, items}` - On success, returns the input items
* `{:error, error}` - On failure
"""
@spec list_input_items(String.t(), keyword()) :: {:ok, map()} | {:error, any()}
def list_input_items(response_id, opts \\ []) do
client = opts[:client] || Client.new(opts)
query = Map.new(for {k, v} <- opts, k in [:before, :after, :limit, :order], do: {k, v})
Client.request(client, :get, "/responses/#{response_id}/input_items", nil, query)
end
@doc ~S"""
Extracts text deltas from a streaming response.
This is a convenience function that returns a stream of text chunks as they arrive,
useful for real-time display of model outputs. The function ensures text is not duplicated
in the final output.
## Parameters
* `stream` - The stream from OpenAI.Responses.stream/3
## Returns
* A stream of text deltas
## Examples
stream = OpenAI.Responses.stream("gpt-4o", "Tell me a story")
text_stream = OpenAI.Responses.text_deltas(stream)
# Print text deltas as they arrive (real-time output)
text_stream
|> Stream.each(fn delta ->
IO.write(delta)
end)
|> Stream.run()
IO.puts("") # Add a newline at the end
# Create a typing effect
stream = OpenAI.Responses.stream("gpt-4o", "Tell me a story")
text_stream = OpenAI.Responses.text_deltas(stream)
text_stream
|> Stream.each(fn delta ->
IO.write(delta)
Process.sleep(10) # Add delay for typing effect
end)
|> Stream.run()
"""
@spec text_deltas(Enumerable.t()) :: Enumerable.t(String.t())
def text_deltas(stream) do
OpenAI.Responses.Stream.text_deltas(stream)
end
@doc ~S"""
Collects a complete response from a streaming response.
This is a convenience function that consumes a stream and returns a complete response,
similar to what would be returned by the non-streaming API. All events are processed
and combined into a final response object.
## Parameters
* `stream` - The stream from OpenAI.Responses.stream/3
## Returns
* The complete response map
## Examples
# Get a streaming response
stream = OpenAI.Responses.stream("gpt-4o", "Tell me a story")
# Collect all events into a single response object
response = OpenAI.Responses.collect_stream(stream)
# Process the complete response
text = OpenAI.Responses.Helpers.output_text(response)
IO.puts(text)
"""
@spec collect_stream(Enumerable.t()) :: map()
def collect_stream(stream) do
OpenAI.Responses.Stream.collect(stream)
end
# Private helpers
defp prepare_create_payload(model, input, opts) do
base = %{
model: model,
input: prepare_input(input)
}
Enum.reduce(opts, base, fn
{:tools, tools}, acc ->
Map.put(acc, :tools, prepare_tools(tools))
{key, value}, acc
when key in [
:instructions,
:temperature,
:max_output_tokens,
:stream,
:previous_response_id,
:reasoning,
:store,
:tool_choice,
:top_p,
:truncation,
:user,
:metadata,
:text,
:parallel_tool_calls
] ->
Map.put(acc, key, value)
_, acc ->
acc
end)
end
defp prepare_input(input) when is_binary(input), do: input
defp prepare_input(input), do: input
defp prepare_tools(tools) when is_list(tools), do: tools
defp prepare_tools(tool) when is_map(tool), do: [tool]
defp prepare_tools(_), do: []
@doc ~S"""
Creates a response with structured output.
This function is similar to `create/3` but automatically parses the response
according to the provided schema and returns the parsed data.
## Parameters
* `model` - The model ID to use (e.g., "gpt-4o")
* `input` - The text prompt or structured input message
* `schema` - The schema definition for structured output
* `opts` - Optional parameters for the request
* `:schema_name` - Optional name for the schema (default: "data")
* All other options supported by `create/3`
## Returns
* `{:ok, parsed_data}` - On success, returns the parsed data
* `{:error, error}` - On failure
## Examples
# Define a schema
calendar_event_schema = OpenAI.Responses.Schema.object(%{
name: :string,
date: :string,
participants: {:array, :string}
})
# Create a response with structured output
{:ok, event} = OpenAI.Responses.parse(
"gpt-4o",
"Alice and Bob are going to a science fair on Friday.",
calendar_event_schema,
schema_name: "event"
)
# Access the parsed data
IO.puts("Event: #{event["name"]} on #{event["date"]}")
IO.puts("Participants: #{Enum.join(event["participants"], ", ")}")
"""
@spec parse(String.t(), String.t() | map() | list(), map(), keyword()) ::
{:ok, map()} | {:error, any()}
def parse(model, input, schema, opts \\ []) do
schema_name = Keyword.get(opts, :schema_name, "data")
strict = Keyword.get(opts, :strict, true)
# Prepare the text format with the schema according to the new API format
text_format = %{
format: %{
type: "json_schema",
name: schema_name,
schema: schema,
strict: strict
}
}
# Construct a system message that instructs the model to extract structured data
system_message = "Extract the #{schema_name} information."
# Add the text format and system message to the options
opts = opts
|> Keyword.put(:text, text_format)
|> Keyword.put(:instructions, system_message)
case create(model, input, opts) do
{:ok, response} ->
# Extract the parsed data from the response
case extract_parsed_data(response) do
{:ok, data} -> {:ok, data}
{:error, error} -> {:error, error}
end
error ->
error
end
end
@doc ~S"""
Creates a streaming response with structured output.
This function is similar to `stream/3` but automatically parses each chunk
according to the provided schema.
## Parameters
* `model` - The model ID to use (e.g., "gpt-4o")
* `input` - The text prompt or structured input message
* `schema` - The schema definition for structured output
* `opts` - Optional parameters for the request
* `:schema_name` - Optional name for the schema (default: "data")
* All other options supported by `stream/3`
## Returns
* A stream that yields parsed data chunks
## Examples
# Define a schema
math_reasoning_schema = OpenAI.Responses.Schema.object(%{
steps: {:array, OpenAI.Responses.Schema.object(%{
explanation: :string,
output: :string
})},
final_answer: :string
})
# Stream a response with structured output
stream = OpenAI.Responses.parse_stream(
"gpt-4o",
"Solve 8x + 7 = -23",
math_reasoning_schema,
schema_name: "math_reasoning"
)
# Process the stream
Enum.each(stream, fn chunk ->
IO.inspect(chunk)
end)
"""
@spec parse_stream(String.t(), String.t() | map() | list(), map(), keyword()) ::
Enumerable.t()
def parse_stream(model, input, schema, opts \\ []) do
schema_name = Keyword.get(opts, :schema_name, "data")
strict = Keyword.get(opts, :strict, true)
# Prepare the text format with the schema according to the new API format
text_format = %{
format: %{
type: "json_schema",
name: schema_name,
schema: schema,
strict: strict
}
}
# Construct a system message that instructs the model to extract structured data
system_message = "Extract the #{schema_name} information."
# Add the text format and system message to the options
opts = opts
|> Keyword.put(:text, text_format)
|> Keyword.put(:instructions, system_message)
# Get the stream
stream = stream(model, input, opts)
# Transform the stream to extract parsed data from each chunk
Stream.map(stream, fn chunk ->
case extract_parsed_data_from_chunk(chunk) do
{:ok, data} -> data
# Return the original chunk if parsing fails
{:error, _} -> chunk
end
end)
end
# Helper function to extract parsed data from a response
defp extract_parsed_data(response) do
case response do
# Check for output_text in the new API format
%{"output_text" => output_text} when is_binary(output_text) ->
try do
{:ok, Jason.decode!(output_text)}
rescue
e -> {:error, "Failed to parse JSON from output_text: #{inspect(e)}"}
end
# Check for structured output in the output array
%{"output" => [%{"content" => [%{"type" => "output_text", "text" => json_text}]}]} ->
try do
{:ok, Jason.decode!(json_text)}
rescue
e -> {:error, "Failed to parse JSON from output content: #{inspect(e)}"}
end
# Check for structured output in a message content array
%{"output" => output} when is_list(output) ->
# Look for a message with JSON content
Enum.find_value(output, {:error, "No parsed data found"}, fn item ->
case item do
%{"content" => content} when is_list(content) ->
# Look for text content that might be JSON
Enum.find_value(content, nil, fn
%{"type" => "output_text", "text" => text} ->
try do
{:ok, Jason.decode!(text)}
rescue
_ -> nil
end
_ -> nil
end)
_ ->
nil
end
end)
# Try direct content access if it's a simple response
%{"content" => content} when is_binary(content) ->
try do
{:ok, Jason.decode!(content)}
rescue
_ -> {:error, "Content is not valid JSON"}
end
_ ->
{:error, "Invalid response format"}
end
end
# Helper function to extract parsed data from a stream chunk
defp extract_parsed_data_from_chunk(chunk) do
case chunk do
# Handle output_text delta in the new API format
%{"delta" => %{"output_text" => output_text}} when is_binary(output_text) and output_text != "" ->
try do
{:ok, Jason.decode!(output_text)}
rescue
_ -> {:error, "Failed to parse JSON from output_text chunk"}
end
# Handle structured output in delta content for output_text type
%{"delta" => %{"content" => [%{"type" => "output_text", "text" => json_text}]}} ->
try do
{:ok, Jason.decode!(json_text)}
rescue
_ -> {:error, "Failed to parse JSON from chunk"}
end
# Handle any text in content array that might be JSON
%{"delta" => %{"content" => content}} when is_list(content) ->
# Look for text content that might be JSON
Enum.find_value(content, {:error, "No parsed data found"}, fn
%{"type" => "output_text", "text" => text} ->
try do
{:ok, Jason.decode!(text)}
rescue
_ -> nil
end
_ -> nil
end)
# Handle text chunks that might contain JSON
%{"delta" => %{"content" => content}} when is_binary(content) and content != "" ->
# We can't parse partial JSON, so we'll return an error
# The complete JSON will be handled when the stream is collected
{:error, "Partial content chunk"}
_ ->
{:error, "No parsed data in chunk"}
end
end
end