Skip to main content

lib/pi/req_llm/provider.ex

case Code.ensure_compiled(ReqLLM.Provider) do
  {:module, ReqLLM.Provider} ->
    defmodule Pi.ReqLLM.Provider do
      @moduledoc "ReqLLM provider for the active Pi model."

      @behaviour ReqLLM.Provider

      alias Pi.Protocol.LLM.Message
      alias ReqLLM.Context
      alias ReqLLM.Response

      def provider_id, do: :pi

      def prepare_request(:chat, model, %Context{} = context, opts) do
        case Pi.LLM.complete(messages(context), opts) do
          {:ok, text} -> {:ok, request_for(response(model, context, text))}
          {:error, reason} -> {:error, RuntimeError.exception(message: inspect(reason))}
        end
      end

      def prepare_request(:chat, model, messages, opts) do
        with {:ok, context} <- Context.normalize(messages, opts) do
          prepare_request(:chat, model, context, opts)
        end
      end

      def prepare_request(_operation, _model, _input, _opts) do
        {:error, RuntimeError.exception(message: "Pi ReqLLM provider only supports chat")}
      end

      def attach(request, _model, _opts), do: request
      def encode_body(request), do: request
      def build_body(_request), do: %{}
      def decode_response({request, response}), do: {request, response}

      defp messages(%Context{} = context) do
        Enum.map(context.messages, fn message ->
          Message.from_map!(%{
            role: message.role,
            content: text_content(message.content)
          })
        end)
      end

      defp text_content(content) when is_list(content) do
        Enum.map_join(content, fn
          %{type: :text, text: text} when is_binary(text) -> text
          part -> inspect(part)
        end)
      end

      defp request_for(response) do
        req_response = %Req.Response{status: 200, body: response}

        Req.new()
        |> Req.Request.append_request_steps(
          pi_complete: fn request -> {request, req_response} end
        )
      end

      defp response(model, context, text) do
        message = Context.assistant(text)

        %Response{
          id: "pi_#{System.unique_integer([:positive])}",
          model: model.id || model.model || "current",
          context: Context.append(context, message),
          message: message,
          usage: nil,
          finish_reason: :stop,
          provider_meta: %{provider: :pi}
        }
      end
    end

  _ ->
    :ok
end