Skip to content
Open
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
34 changes: 34 additions & 0 deletions examples/openai_compatible_example.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Sample script demonstrating AI.generate_text usage with an openai-compatible api
# Run from the elixir-ai-sdk root directory with:
# mix run examples/openai_compatible_example.exs

# Make sure you've set the GOOGLE_API_KEY environment variable

defmodule OpenAICompatibleExample do
def run do
IO.puts("Starting generate_text example...\n")

api_key = System.get_env("GOOGLE_API_KEY")

if is_nil(api_key) do
IO.puts("Error: GOOGLE_API_KEY environment variable not set")
System.halt(1)
end

{:ok, result} =
AI.generate_text(%{
prompt: "is elixir a statically typed programming language?",
model:
AI.openai_compatible(
"gemini-2.5-flash",
base_url: "https://generativelanguage.googleapis.com/v1beta/openai",
api_key: api_key
)
})

IO.puts(result.text)
end
end

# Run the example
OpenAICompatibleExample.run()
12 changes: 6 additions & 6 deletions lib/ai.ex
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,8 @@ defmodule AI do
model = AI.openai_compatible("gpt-3.5-turbo", base_url: "https://api.example.com")

# With API key
model = AI.openai_compatible("gpt-4",
base_url: "https://api.openai.com",
model = AI.openai_compatible("gpt-4",
base_url: "https://api.openai.com/v1",
api_key: System.get_env("OPENAI_API_KEY")
)
"""
Expand Down Expand Up @@ -220,7 +220,7 @@ defmodule AI do
api_key = Map.get(opts, :api_key) || System.get_env("OPENAI_API_KEY")

# Get base URL from options or use default
base_url = Map.get(opts, :base_url, "https://api.openai.com")
base_url = Map.get(opts, :base_url, "https://api.openai.com/v1")

# Extract settings from options
settings =
Expand All @@ -236,7 +236,7 @@ defmodule AI do

# Configure URL function
url_fn = fn %{path: path} ->
"#{String.trim_trailing(base_url, "/")}/v1#{path}"
"#{String.trim_trailing(base_url, "/")}#{path}"
end

# Create the config
Expand Down Expand Up @@ -275,7 +275,7 @@ defmodule AI do
api_key = Map.get(opts, :api_key) || System.get_env("OPENAI_API_KEY")

# Get base URL from options or use default
base_url = Map.get(opts, :base_url, "https://api.openai.com")
base_url = Map.get(opts, :base_url, "https://api.openai.com/v1")

# Extract settings from options
settings = Map.take(opts, [])
Expand All @@ -290,7 +290,7 @@ defmodule AI do

# Configure URL function
url_fn = fn %{path: path} ->
"#{String.trim_trailing(base_url, "/")}/v1#{path}"
"#{String.trim_trailing(base_url, "/")}#{path}"
end

# Create the config
Expand Down
4 changes: 2 additions & 2 deletions lib/ai/providers/openai_compatible/chat_language_model.ex
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ defmodule AI.Providers.OpenAICompatible.ChatLanguageModel do
request_body = add_optional_params(request_body, opts)

# Make API request
case make_api_request(model.provider, "/v1/chat/completions", request_body) do
case make_api_request(model.provider, "/chat/completions", request_body) do
{:ok, response} ->
Comment on lines +72 to 73
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Possible “//” in URL when base_url ends with a slash

make_api_request/3 blindly concatenates:

url = "#{provider.base_url}#{path}"

If a caller supplies base_url: "https://api.example.com/v1/", the final URL becomes
https://api.example.com/v1//chat/completions, which many servers treat differently or reject.

Guard against this by trimming one side or, better, using Path.join/2-style logic:

-url = "#{provider.base_url}#{path}"
+url = "#{String.trim_trailing(provider.base_url, "/")}#{path}"

You might also extract "/chat/completions" into a module attribute to avoid the duplication seen here and at Line 247.

🤖 Prompt for AI Agents
In lib/ai/providers/openai_compatible/chat_language_model.ex around lines 72 to
73, the URL construction in make_api_request/3 concatenates base_url and path
directly, which can cause double slashes if base_url ends with a slash. Fix this
by trimming the trailing slash from base_url or the leading slash from path
before concatenation, or implement Path.join/2-style logic to join them safely.
Additionally, extract the "/chat/completions" string into a module attribute to
avoid duplication and improve maintainability.

# Process API response and return result
process_response(response)
Expand Down Expand Up @@ -244,7 +244,7 @@ defmodule AI.Providers.OpenAICompatible.ChatLanguageModel do
request_body = add_optional_params(request_body, opts)

# Create URL and headers
url = "#{model.provider.base_url}/v1/chat/completions"
url = "#{model.provider.base_url}/chat/completions"
headers = ensure_headers_map(model.provider.headers)

# Get the EventSource module to use (allows mocking)
Expand Down
16 changes: 8 additions & 8 deletions test/ai/openai_compatible_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ defmodule AI.OpenAICompatibleTest do
end

test "creates an OpenAI-compatible model with the required parameters" do
model = AI.openai_compatible("gpt-3.5-turbo", base_url: "https://api.example.com")
model = AI.openai_compatible("gpt-3.5-turbo", base_url: "https://api.example.com/v1")

# Verify the model is a ChatLanguageModel struct
assert %ChatLanguageModel{} = model
Expand All @@ -25,13 +25,13 @@ defmodule AI.OpenAICompatibleTest do

# Verify the provider is set correctly
assert %Provider{} = model.provider
assert model.provider.base_url == "https://api.example.com"
assert model.provider.base_url == "https://api.example.com/v1"
end

test "handles both keyword list and map for options" do
# Test with keyword list
model1 = AI.openai_compatible("gpt-3.5-turbo", base_url: "https://api.example.com")
assert model1.provider.base_url == "https://api.example.com"
model1 = AI.openai_compatible("gpt-3.5-turbo", base_url: "https://api.example.com/v1")
assert model1.provider.base_url == "https://api.example.com/v1"

# Test with map
model2 = AI.openai_compatible("gpt-3.5-turbo", %{base_url: "https://api.other.com"})
Expand All @@ -41,7 +41,7 @@ defmodule AI.OpenAICompatibleTest do
test "sets additional options correctly" do
model =
AI.openai_compatible("gpt-3.5-turbo",
base_url: "https://api.example.com",
base_url: "https://api.example.com/v1",
api_key: "test-api-key",
headers: %{"X-Custom-Header" => "test-value"},
supports_image_urls: true,
Expand Down Expand Up @@ -91,7 +91,7 @@ defmodule AI.OpenAICompatibleTest do
end)

# Create an OpenAI-compatible model
model = AI.openai_compatible("gpt-3.5-turbo", base_url: "https://api.example.com")
model = AI.openai_compatible("gpt-3.5-turbo", base_url: "https://api.example.com/v1")

# Use the model with generate_text
{:ok, result} =
Expand Down Expand Up @@ -164,7 +164,7 @@ defmodule AI.OpenAICompatibleTest do
end)

# Create an OpenAI-compatible model
model = AI.openai_compatible("gpt-3.5-turbo", base_url: "https://api.example.com")
model = AI.openai_compatible("gpt-3.5-turbo", base_url: "https://api.example.com/v1")

# Use the model with generate_text using a prompt parameter instead of messages
{:ok, result} =
Expand Down Expand Up @@ -230,7 +230,7 @@ defmodule AI.OpenAICompatibleTest do
end)

# Create an OpenAI-compatible model
model = AI.openai_compatible("gpt-3.5-turbo", base_url: "https://api.example.com")
model = AI.openai_compatible("gpt-3.5-turbo", base_url: "https://api.example.com/v1")

# Use the model with generate_text using both system and prompt parameters
{:ok, result} =
Expand Down
2 changes: 1 addition & 1 deletion test/ai/openai_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ defmodule AI.OpenAITest do
end

test "allows custom base URL override" do
model = AI.openai("gpt-4", base_url: "https://custom-openai-api.com")
model = AI.openai("gpt-4", base_url: "https://custom-openai-api.com/v1")

# Test the URL function
url = model.config.url.(%{path: "/chat/completions"})
Expand Down
4 changes: 2 additions & 2 deletions test/ai/providers/openai_compatible/generate_text_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ defmodule AI.Providers.OpenAICompatible.GenerateTextTest do
setup do
provider =
Provider.new(%{
base_url: "https://api.example.com",
base_url: "https://api.example.com/v1",
name: "test-provider",
api_key: "test-api-key"
})
Expand Down Expand Up @@ -513,7 +513,7 @@ defmodule AI.Providers.OpenAICompatible.GenerateTextTest do
# Create a provider with custom headers
provider =
Provider.new(%{
base_url: "https://api.example.com",
base_url: "https://api.example.com/v1",
name: "custom-provider",
api_key: "test-api-key",
headers: %{
Expand Down