Skip to content

Commit ee41d3a

Browse files
authored
Add Azure OpenAI provider with configuration options and tests (#301)
* Add Azure OpenAI provider with configuration options and tests * Make Rubocop happy :)
1 parent efad474 commit ee41d3a

30 files changed

+2603
-2
lines changed

lib/active_agent/concerns/provider.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ module Provider
99

1010
# "Your tacky and I hate you" - Billy, https://youtu.be/dsheboxJNgQ?si=tzDlJ7sdSxM4RjSD
1111
PROVIDER_SERVICE_NAMES_REMAPS = {
12-
"Openrouter" => "OpenRouter",
13-
"Openai" => "OpenAI"
12+
"Openrouter" => "OpenRouter",
13+
"Openai" => "OpenAI",
14+
"AzureOpenai" => "AzureOpenAI",
15+
"Azureopenai" => "AzureOpenAI"
1416
}
1517

1618
included do
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "options"
4+
require_relative "../open_ai/chat/_types"
5+
require_relative "../open_ai/embedding/_types"
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "../open_ai/options"
4+
5+
module ActiveAgent
6+
module Providers
7+
module Azure
8+
# Configuration options for Azure OpenAI Service.
9+
#
10+
# Azure OpenAI uses a different authentication and endpoint structure than standard OpenAI:
11+
# - Endpoint: https://{resource}.openai.azure.com/openai/deployments/{deployment}/
12+
# - Authentication: api-key header instead of Authorization: Bearer
13+
# - API Version: Required query parameter
14+
#
15+
# @example Configuration
16+
# options = Azure::Options.new(
17+
# api_key: ENV["AZURE_OPENAI_API_KEY"],
18+
# azure_resource: "mycompany",
19+
# deployment_id: "gpt-4-deployment",
20+
# api_version: "2024-10-21"
21+
# )
22+
class Options < ActiveAgent::Providers::OpenAI::Options
23+
DEFAULT_API_VERSION = "2024-10-21"
24+
25+
attribute :azure_resource, :string
26+
attribute :deployment_id, :string
27+
attribute :api_version, :string, fallback: DEFAULT_API_VERSION
28+
29+
validates :azure_resource, presence: true
30+
validates :deployment_id, presence: true
31+
32+
def initialize(kwargs = {})
33+
kwargs = kwargs.deep_symbolize_keys if kwargs.respond_to?(:deep_symbolize_keys)
34+
kwargs[:api_version] ||= resolve_api_version(kwargs)
35+
super(kwargs)
36+
end
37+
38+
# Returns Azure-specific headers for authentication.
39+
#
40+
# Azure uses api-key header instead of Authorization: Bearer.
41+
#
42+
# @return [Hash] headers including api-key
43+
def extra_headers
44+
{ "api-key" => api_key }
45+
end
46+
47+
# Returns Azure-specific query parameters.
48+
#
49+
# Azure requires api-version as a query parameter.
50+
#
51+
# @return [Hash] query parameters including api-version
52+
def extra_query
53+
{ "api-version" => api_version }
54+
end
55+
56+
# Builds the base URL for Azure OpenAI API requests.
57+
#
58+
# @return [String] the Azure OpenAI endpoint URL
59+
def base_url
60+
"https://#{azure_resource}.openai.azure.com/openai/deployments/#{deployment_id}"
61+
end
62+
63+
private
64+
65+
def resolve_api_key(kwargs)
66+
kwargs[:api_key] ||
67+
kwargs[:access_token] ||
68+
ENV["AZURE_OPENAI_API_KEY"] ||
69+
ENV["AZURE_OPENAI_ACCESS_TOKEN"]
70+
end
71+
72+
def resolve_api_version(kwargs)
73+
kwargs[:api_version] ||
74+
ENV["AZURE_OPENAI_API_VERSION"] ||
75+
DEFAULT_API_VERSION
76+
end
77+
78+
# Not used as part of Azure OpenAI
79+
def resolve_organization_id(_settings) = nil
80+
def resolve_project_id(_settings) = nil
81+
end
82+
end
83+
end
84+
end
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Azure OpenAI, alias for AzureOpenAI service name resolution
2+
require_relative "azure_provider"
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Azure OpenAI, alias for :azure_openai provider reference
2+
require_relative "azure_provider"
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
require_relative "_base_provider"
2+
3+
require_gem!(:openai, __FILE__)
4+
5+
require_relative "open_ai_provider"
6+
require_relative "azure/_types"
7+
8+
module ActiveAgent
9+
module Providers
10+
# Provider for Azure OpenAI Service via OpenAI-compatible API.
11+
#
12+
# Azure OpenAI uses the same API structure as OpenAI but with different
13+
# authentication (api-key header) and endpoint configuration (resource + deployment).
14+
#
15+
# @example Configuration in active_agent.yml
16+
# azure_openai:
17+
# service: "AzureOpenAI"
18+
# api_key: <%= ENV["AZURE_OPENAI_API_KEY"] %>
19+
# azure_resource: "mycompany"
20+
# deployment_id: "gpt-4-deployment"
21+
# api_version: "2024-10-21"
22+
#
23+
# @see OpenAI::ChatProvider
24+
class AzureProvider < OpenAI::ChatProvider
25+
# @return [String]
26+
def self.service_name
27+
"AzureOpenAI"
28+
end
29+
30+
# @return [Class]
31+
def self.options_klass
32+
Azure::Options
33+
end
34+
35+
# @return [ActiveModel::Type::Value]
36+
def self.prompt_request_type
37+
OpenAI::Chat::RequestType.new
38+
end
39+
40+
# @return [ActiveModel::Type::Value]
41+
def self.embed_request_type
42+
OpenAI::Embedding::RequestType.new
43+
end
44+
45+
# Returns a configured Azure OpenAI client.
46+
#
47+
# Uses a custom client subclass that handles Azure-specific authentication
48+
# (api-key header instead of Authorization: Bearer).
49+
#
50+
# @return [AzureClient] the configured Azure client
51+
def client
52+
@client ||= AzureClient.new(
53+
api_key: options.api_key,
54+
base_url: options.base_url,
55+
api_version: options.api_version,
56+
max_retries: options.max_retries,
57+
timeout: options.timeout,
58+
initial_retry_delay: options.initial_retry_delay,
59+
max_retry_delay: options.max_retry_delay
60+
)
61+
end
62+
63+
# Custom OpenAI client for Azure OpenAI Service.
64+
#
65+
# Azure uses different authentication headers (api-key instead of Authorization: Bearer)
66+
# and requires api-version as a query parameter on all requests.
67+
class AzureClient < ::OpenAI::Client
68+
# @return [String]
69+
attr_reader :api_version
70+
71+
# Creates a new Azure OpenAI client.
72+
#
73+
# @param api_key [String] Azure OpenAI API key
74+
# @param base_url [String] Azure endpoint URL
75+
# @param api_version [String] API version (e.g., "2024-10-21")
76+
# @param max_retries [Integer] Maximum retry attempts
77+
# @param timeout [Float] Request timeout in seconds
78+
# @param initial_retry_delay [Float] Initial delay between retries
79+
# @param max_retry_delay [Float] Maximum delay between retries
80+
def initialize(
81+
api_key:,
82+
base_url:,
83+
api_version:,
84+
max_retries: self.class::DEFAULT_MAX_RETRIES,
85+
timeout: self.class::DEFAULT_TIMEOUT_IN_SECONDS,
86+
initial_retry_delay: self.class::DEFAULT_INITIAL_RETRY_DELAY,
87+
max_retry_delay: self.class::DEFAULT_MAX_RETRY_DELAY
88+
)
89+
@api_version = api_version
90+
91+
super(
92+
api_key: api_key,
93+
base_url: base_url,
94+
max_retries: max_retries,
95+
timeout: timeout,
96+
initial_retry_delay: initial_retry_delay,
97+
max_retry_delay: max_retry_delay
98+
)
99+
end
100+
101+
private
102+
103+
# Azure uses api-key header instead of Authorization: Bearer.
104+
#
105+
# @return [Hash{String=>String}]
106+
def auth_headers
107+
return {} if @api_key.nil?
108+
109+
{ "api-key" => @api_key }
110+
end
111+
112+
# Builds request with Azure-specific query parameters.
113+
#
114+
# Injects api-version into extra_query for all requests.
115+
#
116+
# @param req [Hash] Request parameters
117+
# @param opts [Hash] Request options
118+
# @return [Hash] Built request
119+
def build_request(req, opts)
120+
# Inject api-version into extra_query
121+
opts = opts.dup
122+
opts[:extra_query] = (opts[:extra_query] || {}).merge("api-version" => @api_version)
123+
124+
super(req, opts)
125+
end
126+
end
127+
end
128+
129+
# Aliases for provider loading with different service name variations
130+
AzureOpenAIProvider = AzureProvider
131+
AzureOpenaiProvider = AzureProvider
132+
end
133+
end
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Azure OpenAI, alternative naming for consistency with other provider aliases
2+
require_relative "azure_provider"

test/fixtures/vcr_cassettes/integration/azure/common_format/instructions_test/developer_message.yml

Lines changed: 84 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)