lib/genai_providers/open_ai/encoder_protocol.ex

defprotocol GenAI.Provider.OpenAI.EncoderProtocol do
  @moduledoc """
  Encoders use their module's EncoderProtocol to prep messages and tool definitions
  To make future extensibility for third parties straight forward. If a new message
  type, or tool type is added one simply needs to implement a EncoderProtocol for it
  and most cases you can simply cast it to generic known type and then invoke the protocol
  again.
  """
  def encode(subject, model, session, context, options)
end


#-----------------------------
# GenAI.Tool
#-----------------------------
defimpl GenAI.Provider.OpenAI.EncoderProtocol, for: GenAI.Tool do
  def encode(subject, model, session, context, options) do
    encoded = %{
      type: :function,
      function: %{
        name: subject.name,
        description: subject.description,
        parameters: subject.parameters
      }
    }
    {:ok, {encoded, session}}
  end
end

#-----------------------------
# GenAI.Message
#-----------------------------
defimpl GenAI.Provider.OpenAI.EncoderProtocol, for: GenAI.Message do
  def content(content)
  def content(content) when is_bitstring(content) do
    %{type: :text, text: content}
  end
  def content(%GenAI.Message.Content.TextContent{} = content) do
    %{type: :text, text: content.text}
  end
  def content(%GenAI.Message.Content.ImageContent{} = content) do
    {:ok, encoded} = GenAI.Message.Content.ImageContent.base64(content)
    base64 = "data:image/#{content.type};base64," <> encoded
    %{type: :image_url, image_url: %{url:  base64}}
  end
  
  def encode(subject, model, session, context, options) do
    encoded =
      case subject.content do
        x when is_bitstring(x) ->
          %{role: subject.role, content: subject.content}
        x when is_list(x) ->
          content_list = Enum.map(x, &content/1)
          %{role: subject.role, content: content_list}
      end
    
    {:ok, {encoded, session}}
  end
end

#-----------------------------
# GenAI.Message.ToolResponse
#-----------------------------
defimpl GenAI.Provider.OpenAI.EncoderProtocol, for: GenAI.Message.ToolResponse do
  def encode(subject, model, session, context, options) do
    encoded = %{
      role: :tool,
      tool_call_id: subject.tool_call_id,
      content: Jason.encode!(subject.tool_response)
    }
    {:ok, {encoded, session}}
  end
end

#-----------------------------
# GenAI.Message.ToolUsage
#-----------------------------
defimpl GenAI.Provider.OpenAI.EncoderProtocol, for: GenAI.Message.ToolUsage do
  
  def encode_call(
        %GenAI.Message.ToolCall{
          id: id,
          type: type,
          tool_name: tool_name,
          arguments: arguments
        }
      ) do
    %{
      function: %{name: tool_name, arguments: Jason.encode!(arguments)},
      id: id,
      type: type
    }
  end
  
  def encode(subject, model, session, context, options) do
    tool_calls = Enum.map(subject.tool_calls,& encode_call/1)
    encoded = %{
      role: subject.role,
      content: subject.content,
      tool_calls: tool_calls
    }
    {:ok, {encoded, session}}
  end
end