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
6 changes: 4 additions & 2 deletions lib/active_agent/concerns/provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ module Provider

# "Your tacky and I hate you" - Billy, https://youtu.be/dsheboxJNgQ?si=tzDlJ7sdSxM4RjSD
PROVIDER_SERVICE_NAMES_REMAPS = {
"Openrouter" => "OpenRouter",
"Openai" => "OpenAI"
"Openrouter" => "OpenRouter",
"Openai" => "OpenAI",
"AzureOpenai" => "AzureOpenAI",
"Azureopenai" => "AzureOpenAI"
}

included do
Expand Down
5 changes: 5 additions & 0 deletions lib/active_agent/providers/azure/_types.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# frozen_string_literal: true

require_relative "options"
require_relative "../open_ai/chat/_types"
require_relative "../open_ai/embedding/_types"
84 changes: 84 additions & 0 deletions lib/active_agent/providers/azure/options.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# frozen_string_literal: true

require_relative "../open_ai/options"

module ActiveAgent
module Providers
module Azure
# Configuration options for Azure OpenAI Service.
#
# Azure OpenAI uses a different authentication and endpoint structure than standard OpenAI:
# - Endpoint: https://{resource}.openai.azure.com/openai/deployments/{deployment}/
# - 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"
# )
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

def initialize(kwargs = {})
kwargs = kwargs.deep_symbolize_keys if kwargs.respond_to?(:deep_symbolize_keys)
kwargs[:api_version] ||= resolve_api_version(kwargs)
super(kwargs)
end

# Returns Azure-specific headers for authentication.
#
# Azure uses api-key header instead of Authorization: Bearer.
#
# @return [Hash] headers including api-key
def extra_headers
{ "api-key" => api_key }
end

# Returns Azure-specific query parameters.
#
# Azure requires api-version as a query parameter.
#
# @return [Hash] query parameters including api-version
def extra_query
{ "api-version" => api_version }
end

# Builds the base URL for Azure OpenAI API requests.
#
# @return [String] the Azure OpenAI endpoint URL
def base_url
"https://#{azure_resource}.openai.azure.com/openai/deployments/#{deployment_id}"
end

private

def resolve_api_key(kwargs)
kwargs[:api_key] ||
kwargs[:access_token] ||
ENV["AZURE_OPENAI_API_KEY"] ||
ENV["AZURE_OPENAI_ACCESS_TOKEN"]
end

def resolve_api_version(kwargs)
kwargs[:api_version] ||
ENV["AZURE_OPENAI_API_VERSION"] ||
DEFAULT_API_VERSION
end

# Not used as part of Azure OpenAI
def resolve_organization_id(_settings) = nil
def resolve_project_id(_settings) = nil
end
end
end
end
2 changes: 2 additions & 0 deletions lib/active_agent/providers/azure_open_ai_provider.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Azure OpenAI, alias for AzureOpenAI service name resolution
require_relative "azure_provider"
2 changes: 2 additions & 0 deletions lib/active_agent/providers/azure_openai_provider.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Azure OpenAI, alias for :azure_openai provider reference
require_relative "azure_provider"
133 changes: 133 additions & 0 deletions lib/active_agent/providers/azure_provider.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
require_relative "_base_provider"

require_gem!(:openai, __FILE__)

require_relative "open_ai_provider"
require_relative "azure/_types"

module ActiveAgent
module Providers
# Provider for Azure OpenAI Service via OpenAI-compatible API.
#
# Azure OpenAI uses the same API structure as OpenAI but with different
# authentication (api-key header) and endpoint configuration (resource + deployment).
#
# @example Configuration in active_agent.yml
# azure_openai:
# service: "AzureOpenAI"
# api_key: <%= ENV["AZURE_OPENAI_API_KEY"] %>
# azure_resource: "mycompany"
# deployment_id: "gpt-4-deployment"
# api_version: "2024-10-21"
#
# @see OpenAI::ChatProvider
class AzureProvider < OpenAI::ChatProvider
# @return [String]
def self.service_name
"AzureOpenAI"
end

# @return [Class]
def self.options_klass
Azure::Options
end

# @return [ActiveModel::Type::Value]
def self.prompt_request_type
OpenAI::Chat::RequestType.new
end

# @return [ActiveModel::Type::Value]
def self.embed_request_type
OpenAI::Embedding::RequestType.new
end

# Returns a configured Azure OpenAI client.
#
# Uses a custom client subclass that handles Azure-specific authentication
# (api-key header instead of Authorization: Bearer).
#
# @return [AzureClient] the configured Azure client
def client
@client ||= AzureClient.new(
api_key: options.api_key,
base_url: options.base_url,
api_version: options.api_version,
max_retries: options.max_retries,
timeout: options.timeout,
initial_retry_delay: options.initial_retry_delay,
max_retry_delay: options.max_retry_delay
)
end

# Custom OpenAI client for Azure OpenAI Service.
#
# Azure uses different authentication headers (api-key instead of Authorization: Bearer)
# and requires api-version as a query parameter on all requests.
class AzureClient < ::OpenAI::Client
# @return [String]
attr_reader :api_version

# Creates a new Azure OpenAI client.
#
# @param api_key [String] Azure OpenAI API key
# @param base_url [String] Azure endpoint URL
# @param api_version [String] API version (e.g., "2024-10-21")
# @param max_retries [Integer] Maximum retry attempts
# @param timeout [Float] Request timeout in seconds
# @param initial_retry_delay [Float] Initial delay between retries
# @param max_retry_delay [Float] Maximum delay between retries
def initialize(
api_key:,
base_url:,
api_version:,
max_retries: self.class::DEFAULT_MAX_RETRIES,
timeout: self.class::DEFAULT_TIMEOUT_IN_SECONDS,
initial_retry_delay: self.class::DEFAULT_INITIAL_RETRY_DELAY,
max_retry_delay: self.class::DEFAULT_MAX_RETRY_DELAY
)
@api_version = api_version

super(
api_key: api_key,
base_url: base_url,
max_retries: max_retries,
timeout: timeout,
initial_retry_delay: initial_retry_delay,
max_retry_delay: max_retry_delay
)
end

private

# Azure uses api-key header instead of Authorization: Bearer.
#
# @return [Hash{String=>String}]
def auth_headers
return {} if @api_key.nil?

{ "api-key" => @api_key }
end

# Builds request with Azure-specific query parameters.
#
# Injects api-version into extra_query for all requests.
#
# @param req [Hash] Request parameters
# @param opts [Hash] Request options
# @return [Hash] Built request
def build_request(req, opts)
# Inject api-version into extra_query
opts = opts.dup
opts[:extra_query] = (opts[:extra_query] || {}).merge("api-version" => @api_version)

super(req, opts)
end
end
end

# Aliases for provider loading with different service name variations
AzureOpenAIProvider = AzureProvider
AzureOpenaiProvider = AzureProvider
end
end
2 changes: 2 additions & 0 deletions lib/active_agent/providers/azureopenai_provider.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Azure OpenAI, alternative naming for consistency with other provider aliases
require_relative "azure_provider"

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading