Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 32 additions & 11 deletions lib/req_llm/providers/openrouter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,18 @@ defmodule ReqLLM.Providers.OpenRouter do
openrouter_plugins: [
type: {:list, :map},
doc: "OpenRouter plugins. Example: [%{id: \"web\"}]"
],
dimensions: [
type: :pos_integer,
doc: "Number of dimensions for embedding models"
],
encoding_format: [
type: {:in, ["float", "base64"]},
doc: "Format for embedding output"
],
input_type: [
type: :string,
doc: "Embedding input type, such as search_query or search_document"
]
]

Expand Down Expand Up @@ -188,17 +200,6 @@ defmodule ReqLLM.Providers.OpenRouter do
)
end

# Override to reject unsupported operations
def prepare_request(:embedding, _model_spec, _input, _opts) do
supported_operations = [:chat, :object]

{:error,
ReqLLM.Error.Invalid.Parameter.exception(
parameter:
"operation: :embedding not supported by #{inspect(__MODULE__)}. Supported operations: #{inspect(supported_operations)}"
)}
end

# Delegate other operations to default implementation
def prepare_request(operation, model_spec, input, opts) do
ReqLLM.Provider.Defaults.prepare_request(__MODULE__, operation, model_spec, input, opts)
Expand Down Expand Up @@ -293,6 +294,7 @@ defmodule ReqLLM.Providers.OpenRouter do
@impl ReqLLM.Provider
def build_body(request) do
ReqLLM.Provider.Defaults.default_build_body(request)
|> add_embedding_options(request.options)
|> translate_tool_choice_format()
|> encode_reasoning_details_in_messages()
|> maybe_put(:models, request.options[:openrouter_models])
Expand All @@ -311,6 +313,25 @@ defmodule ReqLLM.Providers.OpenRouter do
|> add_stream_options(request.options)
end

defp add_embedding_options(body, request_options) do
if request_options[:operation] == :embedding do
put_embedding_options(body, request_options)
else
body
end
end

defp put_embedding_options(body, request_options) do
body
|> maybe_put(:dimensions, embedding_option(request_options, :dimensions))
|> maybe_put(:encoding_format, embedding_option(request_options, :encoding_format))
|> maybe_put(:input_type, embedding_option(request_options, :input_type))
end

defp embedding_option(request_options, key) do
request_options[key] || get_in(request_options, [:provider_options, key])
end

# Helper function for adding OpenRouter-specific body options not covered by defaults
defp add_openrouter_specific_options(body, request_options) do
# Add OpenRouter-specific options that aren't handled by the default encoding
Expand Down
62 changes: 56 additions & 6 deletions test/providers/openrouter_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -767,19 +767,69 @@ defmodule ReqLLM.Providers.OpenRouterTest do
{:ok, model} = ReqLLM.model("openrouter:openai/gpt-4")
context = context_fixture()

# Test unsupported operation for 3-arg version
{:error, error} = OpenRouter.prepare_request(:embedding, model, context, [])
{:error, error} = OpenRouter.prepare_request(:moderation, model, context, [])
assert %ReqLLM.Error.Invalid.Parameter{} = error
assert error.parameter =~ "operation: :embedding not supported"
assert error.parameter =~ "operation: :moderation not supported"

# Test unsupported operation for object with schema
{:ok, schema} = ReqLLM.Schema.compile([])

{:error, error} =
OpenRouter.prepare_request(:embedding, model, context, compiled_schema: schema)
OpenRouter.prepare_request(:moderation, model, context, compiled_schema: schema)

assert %ReqLLM.Error.Invalid.Parameter{} = error
assert error.parameter =~ "operation: :embedding not supported"
assert error.parameter =~ "operation: :moderation not supported"
end

test "prepare_request creates configured embedding request" do
model =
LLMDB.Model.new!(%{
provider: :openrouter,
id: "openai/text-embedding-3-small",
capabilities: %{embeddings: true}
})

opts = [
dimensions: 16,
encoding_format: "float",
provider_options: [input_type: "search_query"]
]

{:ok, request} = OpenRouter.prepare_request(:embedding, model, "Hello", opts)

assert request.url.path == "/embeddings"
assert request.method == :post
assert request.options[:operation] == :embedding
assert request.options[:model] == "openai/text-embedding-3-small"
assert request.options[:text] == "Hello"
assert request.options[:dimensions] == 16
assert request.options[:encoding_format] == "float"
assert request.options[:input_type] == "search_query"
end

test "encode_body includes OpenRouter embedding options" do
mock_request = %Req.Request{
options: [
operation: :embedding,
model: "openai/text-embedding-3-small",
text: "Hello",
dimensions: 16,
encoding_format: "float",
input_type: "search_query",
openrouter_provider: %{order: ["openai"]},
user: "test-user"
]
}

updated_request = OpenRouter.encode_body(mock_request)
decoded = Jason.decode!(updated_request.body)

assert decoded["model"] == "openai/text-embedding-3-small"
assert decoded["input"] == "Hello"
assert decoded["dimensions"] == 16
assert decoded["encoding_format"] == "float"
assert decoded["input_type"] == "search_query"
assert decoded["provider"] == %{"order" => ["openai"]}
assert decoded["user"] == "test-user"
end
end

Expand Down
63 changes: 63 additions & 0 deletions test/req_llm/embedding_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,69 @@ defmodule ReqLLM.EmbeddingTest do
end
end

describe "embed/3 - OpenRouter embeddings" do
setup do
Req.Test.stub(__MODULE__.OpenRouterEmbedUsage, fn conn ->
body = conn.body_params

assert conn.method == "POST"
assert conn.request_path == "/api/v1/embeddings"
assert body["model"] == "openai/text-embedding-3-small"
assert body["input"] == "Hello world"
assert body["dimensions"] == 16
assert body["encoding_format"] == "float"
assert body["input_type"] == "search_query"
assert body["provider"] == %{"order" => ["openai"]}
assert Plug.Conn.get_req_header(conn, "authorization") == ["Bearer test-key"]

Req.Test.json(conn, %{
"data" => [
%{
"embedding" => [0.1, -0.2, 0.3],
"index" => 0,
"object" => "embedding"
}
],
"model" => "text-embedding-3-small",
"object" => "list",
"usage" => %{
"prompt_tokens" => 1,
"total_tokens" => 1
}
})
end)

:ok
end

test "embeds unverified OpenRouter embedding models with inline specs" do
model =
LLMDB.Model.new!(%{
provider: :openrouter,
id: "openai/text-embedding-3-small",
capabilities: %{embeddings: true}
})

{:ok, %{embedding: embedding, usage: usage}} =
Embedding.embed(
model,
"Hello world",
api_key: "test-key",
dimensions: 16,
provider_options: [
input_type: "search_query",
openrouter_provider: %{order: ["openai"]}
],
return_usage: true,
req_http_options: [plug: {Req.Test, __MODULE__.OpenRouterEmbedUsage}]
)

assert embedding == [0.1, -0.2, 0.3]
assert usage.input == 1
assert usage.total_tokens == 1
end
end

describe "embed_many/3 - basic functionality" do
test "validates model before attempting embedding" do
case Embedding.validate_model("openai:text-embedding-3-small") do
Expand Down
Loading