defmodule AgentSea.Embedder.OpenAI do @moduledoc """ OpenAI embeddings adapter (`POST /v1/embeddings`) over `Req` — a remote `AgentSea.Embedder` (no local model). Options: `:api_key` (defaults to `OPENAI_API_KEY`), `:model` (default `text-embedding-3-small`), `:base_url`, `:adapter` (a Req adapter for tests). `dimensions/0` reads `config :agentsea_embeddings, :openai_dimensions` (default 1536 — must match the model). """ @behaviour AgentSea.Embedder @base_url "https://api.openai.com" @impl true def embed(texts, opts) do body = %{model: opts[:model] || "text-embedding-3-small", input: texts} case Req.post(req(opts), url: "/v1/embeddings", json: body) do {:ok, %Req.Response{status: 200, body: %{"data" => data}}} -> # Preserve input order via the response index. vectors = data |> Enum.sort_by(& &1["index"]) |> Enum.map(& &1["embedding"]) {:ok, vectors} {:ok, %Req.Response{status: status, body: body}} -> {:error, {:http_error, status, body}} {:error, reason} -> {:error, reason} end end @impl true def dimensions do Application.get_env(:agentsea_embeddings, :openai_dimensions, 1536) end defp req(opts) do api_key = opts[:api_key] || System.get_env("OPENAI_API_KEY") [ base_url: opts[:base_url] || @base_url, headers: [{"authorization", "Bearer #{api_key || ""}"}] ] |> maybe_put(:adapter, opts[:adapter]) |> Req.new() end defp maybe_put(kw, _key, nil), do: kw defp maybe_put(kw, key, value), do: Keyword.put(kw, key, value) end