defmodule GenAI.Provider.Gemini.Encoder do @base_url "https://generativelanguage.googleapis.com" use GenAI.Model.EncoderBehaviour #-------------------------------------------- # #-------------------------------------------- def api_key(settings, options \\ []) do search_scope = [ options, settings[:model_settings], settings[:provider_settings], settings[:settings], settings[:config_settings], ] api_key = search_scope |> Enum.find_value(& &1[:api_key]) unless api_key do raise GenAI.RequestError, message: "Gemini API key not found in settings or options" end {:ok, api_key} end #-------------------------------------------- # #-------------------------------------------- def endpoint(model, settings, session, context, options) def endpoint(model, settings, session ,_ ,options) do {:ok, model_name} = GenAI.ModelProtocol.name(model) {:ok, api_key} = api_key(settings, options) {:ok, {{:post, "#{@base_url}/v1beta/models/#{model_name}:generateContent?key=#{api_key}"}, session}} end #-------------------------------------------- # #-------------------------------------------- def headers(model, settings, session, context, options) do headers = [{"content-type", "application/json"}] {:ok, {headers, session}} end def default_hyper_params(model, settings, session, context, options) def default_hyper_params(model, settings, session, context, options) do x = [ hyper_param(name: :max_tokens, as: :max_output_tokens), hyper_param(name: :stop_sequence, as: :stop_sequences), hyper_param(name: :temperature), hyper_param(name: :top_k), hyper_param(name: :top_p), ] {:ok, x} end # --------------------------------- # request_body/7 # --------------------------------- # @todo support response modalities @doc "Prepare request body to be passed to inference call." def request_body(model, messages, tools, settings, session, context, options) def request_body(model, messages, tools, settings, session, context, options) do with {:ok, model_name} <- GenAI.ModelProtocol.name(model), {:ok, params} <- hyper_params(model, settings, session, context, options) do tool_declaration = with [_|_] <- tools do [%{function_declarations: tools}] end body = %{contents: messages} |> optional_field(:generation_config, generation_config(params, model, settings)) |> optional_field(:safety_settings, safety_settings(settings)) |> optional_field(:tools, tool_declaration) {:ok, {body, session}} end end # ------------------------- # # ------------------------- defp generation_config(params, model, settings) do config = GenAI.Model.Encoder.DefaultProvider.apply_hyper_params_and_adjust(__MODULE__, %{}, params, model, settings) unless config == %{}, do: config end # ------------------------- # # ------------------------- defp safety_settings(%{safety_settings: nil}), do: nil defp safety_settings(%{safety_settings: []}), do: nil defp safety_settings(%{safety_settings: x}) when is_list(x) or is_map(x) do Enum.map(x, fn {category, threshold} -> %{category: category, threshold: threshold} end ) end #-------------------------------------------- # #-------------------------------------------- # @todo normalize_messages - inject space between like roles #-------------------------------------------- # #-------------------------------------------- def completion_response(json, model, settings, session, context, options) def completion_response(json, model, settings, session, context, options) do with {:ok, provider} <- GenAI.ModelProtocol.provider(model), {:ok, model_name} <- GenAI.ModelProtocol.name(model), %{candidates: candidates} <- json do id = json[:id] choices = candidates |> Enum.map(& completion_choices(id, &1, model, settings, session, context, options)) |> Enum.map(fn {:ok, x} -> x end) completion = GenAI.ChatCompletion.from_json( id: id, model: model_name, provider: provider, choices: choices, usage: GenAI.ChatCompletion.Usage.new([]), details: json ) {:ok, completion} end end def completion_choices(id, json, model, settings, session, context, options) def completion_choices( id, json = %{index: index, content: message_json, finishReason: finish_reason}, model, settings, session, context, options ) do with {:ok, message} <- completion_choice(id, message_json, model, settings, session, context, options) do choice = GenAI.ChatCompletion.Choice.new( id: json[:id], index: index, message: message, finish_reason: finish_reason && String.downcase(finish_reason) ) {:ok, choice} end end def completion_choice(id, json, model, settings, session, context, options) def completion_choice( _, json = %{ role: "model", parts: contents }, _, _, _, _, _ ) do content = completion_message(json) tool_calls = Enum.filter(content, & &1.__struct__ == GenAI.Message.Content.ToolUseContent) # @TODO the protocol must scan content for tool content and seperate it out # when preping for gemini id = json[:id] msg = cond do tool_calls == [] -> GenAI.Message.new(id: id, role: :assistant, content: content) :else -> GenAI.Message.ToolUsage.new(id: id, role: :assistant, content: content, tool_calls: []) end {:ok, msg} end defp gen_unique_call_id do {:ok, short_uuid} = ShortUUID.encode(UUID.uuid4()) "call_#{short_uuid}" end def completion_message(%{parts: content}) when is_bitstring(content) do Enum.map([content], &completion_content/1) end def completion_message(%{parts: content}) when is_list(content) do Enum.map(content, &completion_content/1) end def completion_content(json) def completion_content(%{functionCall: %{name: tool_name, args: arguments}} = json) do id = json[:id] || gen_unique_call_id() %GenAI.Message.Content.ToolUseContent{ id: id, tool_name: tool_name, arguments: arguments } end def completion_content(%{text: text} = json) do %GenAI.Message.Content.TextContent{ system: false, type: :response, text: text, } end @image_mime_types [ "image/jpeg", "image/png", "image/gif", "image/webp", "image/svg+xml", ] def completion_content( %{ inline_data: %{ mime_type: mime_type, data: base64 } } = json) when mime_type in @image_mime_types do media_types = %{ "image/jpeg" => :jpeg, "image/png" => :png, "image/gif" => :gif, "image/webp" => :webp, "image/svg+xml" => :svg, } GenAI.Message.Content.ImageContent.new( {:base64, base64}, source: :gemini, type: media_types[mime_type] ) end end