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
47 changes: 37 additions & 10 deletions lib/active_agent/providers/azure/options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,40 @@ module Azure
# - Authentication: api-key header instead of Authorization: Bearer
# - API Version: Required query parameter
#
# @example Configuration
# options = Azure::Options.new(
# api_key: ENV["AZURE_OPENAI_API_KEY"],
# azure_resource: "mycompany",
# deployment_id: "gpt-4-deployment",
# api_version: "2024-10-21"
# )
# You can configure Azure OpenAI in two ways:
#
# 1. Using azure_resource and deployment_id (standard Azure OpenAI):
# @example
# options = Azure::Options.new(
# api_key: ENV["AZURE_OPENAI_API_KEY"],
# azure_resource: "mycompany",
# deployment_id: "gpt-4-deployment",
# api_version: "2024-10-21"
# )
#
# 2. Using a direct host/base_url (for custom domains or Azure AI Foundry):
# @example
# options = Azure::Options.new(
# api_key: ENV["AZURE_OPENAI_API_KEY"],
# host: "https://mycompany.cognitiveservices.azure.com/openai/deployments/gpt-4",
# api_version: "2024-10-21"
# )
class Options < ActiveAgent::Providers::OpenAI::Options
DEFAULT_API_VERSION = "2024-10-21"

attribute :azure_resource, :string
attribute :deployment_id, :string
attribute :api_version, :string, fallback: DEFAULT_API_VERSION

validates :azure_resource, presence: true
validates :deployment_id, presence: true
validates :azure_resource, presence: true, unless: :explicit_host_provided?
validates :deployment_id, presence: true, unless: :explicit_host_provided?

def initialize(kwargs = {})
kwargs = kwargs.deep_symbolize_keys if kwargs.respond_to?(:deep_symbolize_keys)
kwargs[:api_version] ||= resolve_api_version(kwargs)
# Store explicit host before super processes kwargs
# host is aliased to base_url in parent, so check both
@explicit_host = kwargs[:host] || kwargs[:base_url]
super(kwargs)
end

Expand All @@ -55,13 +69,26 @@ def extra_query

# Builds the base URL for Azure OpenAI API requests.
#
# If a direct host/base_url is provided, uses that directly.
# Otherwise, constructs the URL from azure_resource and deployment_id.
#
# @return [String] the Azure OpenAI endpoint URL
def base_url
"https://#{azure_resource}.openai.azure.com/openai/deployments/#{deployment_id}"
if @explicit_host.present?
@explicit_host
elsif azure_resource.present? && deployment_id.present?
"https://#{azure_resource}.openai.azure.com/openai/deployments/#{deployment_id}"
else
raise ArgumentError, "Either host or azure_resource + deployment_id must be provided"
end
end

private

def explicit_host_provided?
@explicit_host.present?
end

def resolve_api_key(kwargs)
kwargs[:api_key] ||
kwargs[:access_token] ||
Expand Down
124 changes: 124 additions & 0 deletions test/providers/azure/options_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,128 @@ class AzureOptionsTest < ActiveSupport::TestCase
ensure
ENV["AZURE_OPENAI_API_VERSION"] = original_version
end

# Tests for direct host support (Azure AI Foundry, Cognitive Services, custom domains)
test "allows direct host configuration without azure_resource and deployment_id" do
options = ActiveAgent::Providers::Azure::Options.new(
api_key: "test-api-key",
host: "https://mycompany.cognitiveservices.azure.com/openai/deployments/gpt-4",
api_version: "2024-10-21"
)

assert options.valid?, "Expected options to be valid, got errors: #{options.errors.full_messages}"
assert_equal "https://mycompany.cognitiveservices.azure.com/openai/deployments/gpt-4", options.base_url
end

test "uses direct host for base_url when provided" do
custom_host = "https://custom.azure.com/openai/deployments/my-model"
options = ActiveAgent::Providers::Azure::Options.new(
api_key: "test-api-key",
host: custom_host
)

assert_equal custom_host, options.base_url
end

test "uses base_url alias for direct host configuration" do
custom_url = "https://mycompany.cognitiveservices.azure.com/openai/deployments/gpt-4.1"
options = ActiveAgent::Providers::Azure::Options.new(
api_key: "test-api-key",
base_url: custom_url
)

assert_equal custom_url, options.base_url
end

test "prefers host over azure_resource and deployment_id" do
custom_host = "https://custom.azure.com/path"
options = ActiveAgent::Providers::Azure::Options.new(
api_key: "test-api-key",
host: custom_host,
azure_resource: "ignored",
deployment_id: "also-ignored"
)

assert_equal custom_host, options.base_url
end

test "validates presence of azure_resource only when host not provided" do
options = ActiveAgent::Providers::Azure::Options.new(
api_key: "test-api-key",
deployment_id: "gpt-4"
# Missing both host and azure_resource
)

assert_not options.valid?
assert_includes options.errors[:azure_resource], "can't be blank"
end

test "validates presence of deployment_id only when host not provided" do
options = ActiveAgent::Providers::Azure::Options.new(
api_key: "test-api-key",
azure_resource: "mycompany"
# Missing both host and deployment_id
)

assert_not options.valid?
assert_includes options.errors[:deployment_id], "can't be blank"
end

test "skips azure_resource validation when host provided" do
options = ActiveAgent::Providers::Azure::Options.new(
api_key: "test-api-key",
host: "https://custom.azure.com/path"
# No azure_resource - should be valid
)

assert options.valid?, "Expected valid with host, got: #{options.errors.full_messages}"
end

test "skips deployment_id validation when host provided" do
options = ActiveAgent::Providers::Azure::Options.new(
api_key: "test-api-key",
host: "https://custom.azure.com/path"
# No deployment_id - should be valid
)

assert options.valid?, "Expected valid with host, got: #{options.errors.full_messages}"
end

test "is valid with only host and api_key" do
original_version = ENV["AZURE_OPENAI_API_VERSION"]
ENV.delete("AZURE_OPENAI_API_VERSION")

options = ActiveAgent::Providers::Azure::Options.new(
api_key: "test-api-key",
host: "https://myhost.azure.com/path"
)

assert options.valid?, "Expected options to be valid with just host and api_key, got: #{options.errors.full_messages}"
ensure
ENV["AZURE_OPENAI_API_VERSION"] = original_version
end

test "still returns correct extra_headers when using direct host" do
options = ActiveAgent::Providers::Azure::Options.new(
api_key: "my-secret-key",
host: "https://custom.azure.com/path"
)

assert_equal({ "api-key" => "my-secret-key" }, options.extra_headers)
end

test "still returns correct extra_query when using direct host" do
original_version = ENV["AZURE_OPENAI_API_VERSION"]
ENV.delete("AZURE_OPENAI_API_VERSION")

options = ActiveAgent::Providers::Azure::Options.new(
api_key: "test-key",
host: "https://custom.azure.com/path",
api_version: "2025-01-01-preview"
)

assert_equal({ "api-version" => "2025-01-01-preview" }, options.extra_query)
ensure
ENV["AZURE_OPENAI_API_VERSION"] = original_version
end
end