Packages
nous
0.12.11
0.17.0
0.16.6
0.16.5
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.8
0.15.7
0.15.6
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.3
0.14.2
0.14.1
0.14.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.17
0.12.16
0.12.15
0.12.14
0.12.13
0.12.12
0.12.11
0.12.9
0.12.7
0.12.6
0.12.5
0.12.3
0.12.2
0.12.0
0.11.3
0.11.0
0.10.1
0.10.0
0.9.0
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.5.0
AI agent framework for Elixir with multi-provider LLM support
Current section
Files
Jump to
Current section
Files
lib/nous/messages/openai.ex
defmodule Nous.Messages.OpenAI do
@moduledoc """
OpenAI format message conversion.
Handles conversion between internal Message structs and OpenAI API format.
"""
alias Nous.{Message, Usage}
alias Nous.Message.ContentPart
require Logger
@doc """
Convert messages to OpenAI format.
## Examples
iex> messages = [Message.system("Be helpful"), Message.user("Hello")]
iex> Messages.OpenAI.to_format(messages)
[
%{"role" => "system", "content" => "Be helpful"},
%{"role" => "user", "content" => "Hello"}
]
"""
@spec to_format([Message.t()]) :: [map()]
def to_format(messages) when is_list(messages) do
Enum.map(messages, &message_to_openai/1)
end
@doc """
Parse OpenAI response into a Message.
## Examples
iex> response = %{"choices" => [%{"message" => %{"role" => "assistant", "content" => "Hello"}}]}
iex> Messages.OpenAI.from_response(response)
%Message{role: :assistant, content: "Hello"}
"""
@spec from_response(map()) :: Message.t()
def from_response(response) when is_map(response) do
choices = Map.get(response, "choices") || Map.get(response, :choices) || []
choice = List.first(choices)
message_data =
if choice do
Map.get(choice, "message") || Map.get(choice, :message)
end
content =
if message_data do
Map.get(message_data, "content") || Map.get(message_data, :content)
end
reasoning_content =
if message_data do
Map.get(message_data, "reasoning_content") || Map.get(message_data, :reasoning_content)
end
tool_calls =
if message_data do
Map.get(message_data, "tool_calls") || Map.get(message_data, :tool_calls) || []
else
[]
end
usage_data = Map.get(response, "usage") || Map.get(response, :usage)
model_name = Map.get(response, "model") || Map.get(response, :model)
# Convert tool calls
converted_tool_calls = Enum.map(tool_calls, &parse_tool_call/1)
# Build message attributes
attrs = %{
role: :assistant,
metadata: %{
model_name: model_name,
usage: parse_usage(usage_data),
timestamp: DateTime.utc_now()
}
}
attrs = if content && content != "", do: Map.put(attrs, :content, content), else: attrs
attrs =
if reasoning_content && reasoning_content != "",
do: Map.put(attrs, :reasoning_content, reasoning_content),
else: attrs
attrs =
if length(converted_tool_calls) > 0,
do: Map.put(attrs, :tool_calls, converted_tool_calls),
else: attrs
Message.new!(attrs)
end
@doc """
Convert OpenAI format messages to internal Message structs.
"""
@spec from_messages([map()]) :: [Message.t()]
def from_messages(openai_messages) when is_list(openai_messages) do
Enum.map(openai_messages, fn msg ->
role =
case Map.get(msg, :role) || Map.get(msg, "role") do
"system" -> :system
"user" -> :user
"assistant" -> :assistant
"tool" -> :tool
other -> other
end
content = Map.get(msg, :content) || Map.get(msg, "content")
Message.new!(%{role: role, content: content || ""})
end)
end
# Private helpers
defp message_to_openai(%Message{role: :system, content: content}) when is_binary(content) do
%{"role" => "system", "content" => content}
end
defp message_to_openai(%Message{role: :user, metadata: %{content_parts: content_parts}})
when is_list(content_parts) do
openai_content = Enum.map(content_parts, &content_part_to_openai/1)
%{"role" => "user", "content" => openai_content}
end
defp message_to_openai(%Message{role: :user, content: content}) when is_binary(content) do
%{"role" => "user", "content" => content}
end
defp message_to_openai(%Message{
role: :assistant,
content: content,
reasoning_content: reasoning,
tool_calls: tool_calls
}) do
base = %{
"role" => "assistant",
"content" => content || ""
}
base = if reasoning, do: Map.put(base, "reasoning_content", reasoning), else: base
if length(tool_calls) > 0 do
# Assistant message with tool calls
openai_tool_calls = Enum.map(tool_calls, &tool_call_to_openai/1)
Map.put(base, "tool_calls", openai_tool_calls)
else
# Simple assistant message
base
end
end
defp message_to_openai(%Message{
role: :tool,
content: content,
tool_call_id: tool_call_id,
name: _name
}) do
%{"role" => "tool", "content" => content, "tool_call_id" => tool_call_id}
end
defp content_part_to_openai(%ContentPart{type: :text, content: text}) do
%{"type" => "text", "text" => text}
end
defp content_part_to_openai(%ContentPart{type: :image_url, content: url}) do
%{"type" => "image_url", "image_url" => %{"url" => url}}
end
defp content_part_to_openai(%ContentPart{} = part) do
# Fallback: convert to text representation
%{"type" => "text", "text" => ContentPart.to_text([part])}
end
defp tool_call_to_openai(tool_call) when is_map(tool_call) do
%{
"id" => Map.get(tool_call, "id") || Map.get(tool_call, :id),
"type" => "function",
"function" => %{
"name" => Map.get(tool_call, "name") || Map.get(tool_call, :name),
"arguments" =>
Jason.encode!(Map.get(tool_call, "arguments") || Map.get(tool_call, :arguments, %{}))
}
}
end
defp parse_tool_call(tool_call) when is_map(tool_call) do
id = Map.get(tool_call, "id") || Map.get(tool_call, :id)
func = Map.get(tool_call, "function") || Map.get(tool_call, :function)
name = Map.get(func, "name") || Map.get(func, :name)
arguments = Map.get(func, "arguments") || Map.get(func, :arguments)
parsed_args =
case Jason.decode(arguments) do
{:ok, decoded_args} ->
decoded_args
{:error, _} ->
Logger.warning("Failed to decode tool arguments: #{inspect(arguments)}")
%{"error" => "Invalid JSON arguments", "raw" => arguments}
end
%{
"id" => id,
"name" => name,
"arguments" => parsed_args
}
end
defp parse_usage(usage_data) when is_map(usage_data) do
%Usage{
requests: 1,
input_tokens:
Map.get(usage_data, "prompt_tokens") || Map.get(usage_data, :prompt_tokens) || 0,
output_tokens:
Map.get(usage_data, "completion_tokens") || Map.get(usage_data, :completion_tokens) || 0,
total_tokens: Map.get(usage_data, "total_tokens") || Map.get(usage_data, :total_tokens) || 0
}
end
defp parse_usage(nil), do: %Usage{}
end