Class: Legion::Extensions::Llm::AzureFoundry::Provider

Inherits:
Provider
  • Object
show all
Includes:
Provider::OpenAICompatible
Defined in:
lib/legion/extensions/llm/azure_foundry/provider.rb

Overview

Azure AI Foundry and Azure OpenAI hosted provider surface.

Defined Under Namespace

Modules: Capabilities

Constant Summary collapse

DEFAULT_API_VERSION =
'2024-05-01-preview'
MODEL_INFERENCE_SURFACE =
:model_inference
OPENAI_V1_SURFACE =
:openai_v1
CATALOG_CONTEXT_KEYS =

Keys a live model catalog might use to report context length. Azure's own endpoints rarely do (see deployment_limits), so this is a best-effort read.

%i[context_window max_input_tokens context_length].freeze

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.capabilitiesObject



39
# File 'lib/legion/extensions/llm/azure_foundry/provider.rb', line 39

def capabilities = Capabilities

.configuration_optionsObject



28
29
30
31
32
33
34
35
36
37
# File 'lib/legion/extensions/llm/azure_foundry/provider.rb', line 28

def configuration_options
  %i[
    azure_foundry_endpoint
    azure_foundry_api_key
    azure_foundry_bearer_token
    azure_foundry_api_version
    azure_foundry_surface
    azure_foundry_deployments
  ]
end

.configuration_requirementsObject



26
# File 'lib/legion/extensions/llm/azure_foundry/provider.rb', line 26

def configuration_requirements = %i[azure_foundry_endpoint]

.default_tierObject



25
# File 'lib/legion/extensions/llm/azure_foundry/provider.rb', line 25

def default_tier = :cloud

.default_transportObject



24
# File 'lib/legion/extensions/llm/azure_foundry/provider.rb', line 24

def default_transport = :http

.deployment_config(model_id, config:) ⇒ Object



50
51
52
53
54
55
56
57
# File 'lib/legion/extensions/llm/azure_foundry/provider.rb', line 50

def deployment_config(model_id, config:)
  deployments = config&.azure_foundry_deployments
  entries = normalize_deployments(deployments)
  entries.find do |entry|
    [value_for(entry, :deployment), value_for(entry, :model), value_for(entry, :canonical_model_alias)]
      .compact.map(&:to_s).include?(model_id.to_s)
  end
end

.normalize_deployments(deployments) ⇒ Object



59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/legion/extensions/llm/azure_foundry/provider.rb', line 59

def normalize_deployments(deployments)
  case deployments
  when Hash
    deployments.map do |name, |
      value = .to_h
      value[:deployment] ||= name
      value
    end
  else
    Array(deployments).map { |deployment| normalize_deployment_entry(deployment) }
  end
end

.registry_publisherObject



41
42
43
# File 'lib/legion/extensions/llm/azure_foundry/provider.rb', line 41

def registry_publisher
  AzureFoundry.registry_publisher
end

.resolve_model_id(model_id, config: nil) ⇒ Object



45
46
47
48
# File 'lib/legion/extensions/llm/azure_foundry/provider.rb', line 45

def resolve_model_id(model_id, config: nil)
  deployment = deployment_config(model_id, config:)
  value_for(deployment, :deployment) || value_for(deployment, :model) || model_id.to_s
end

.slugObject



23
# File 'lib/legion/extensions/llm/azure_foundry/provider.rb', line 23

def slug = 'azure_foundry'

Instance Method Details

#api_baseObject



131
132
133
134
135
136
137
# File 'lib/legion/extensions/llm/azure_foundry/provider.rb', line 131

def api_base
  endpoint = config.azure_foundry_endpoint.to_s.sub(%r{/*\z}, '')
  return "#{endpoint}/openai/v1" if surface == OPENAI_V1_SURFACE && !endpoint.end_with?('/openai/v1')
  return endpoint.delete_suffix('/models') if surface == MODEL_INFERENCE_SURFACE

  endpoint
end

#chat(messages:, model:, **options) ⇒ Object



200
201
202
203
204
205
206
207
208
209
# File 'lib/legion/extensions/llm/azure_foundry/provider.rb', line 200

def chat(
  messages:,
  model:,
  **options
)
  log.info { "chat request model=#{model} messages=#{messages.size}" }
  complete(messages, tools: options.fetch(:tools, {}), temperature: options[:temperature],
                     model: model_info(model, max_tokens: options[:max_tokens]),
                     params: options.fetch(:params, {}), tool_prefs: options[:tool_prefs])
end

#chat_urlObject



147
# File 'lib/legion/extensions/llm/azure_foundry/provider.rb', line 147

def chat_url = completion_url

#completion_urlObject



146
# File 'lib/legion/extensions/llm/azure_foundry/provider.rb', line 146

def completion_url = path_for('chat/completions')

#count_tokens(messages:, model:, **_provider_options) ⇒ Object



238
239
240
241
242
243
244
245
246
247
248
249
250
# File 'lib/legion/extensions/llm/azure_foundry/provider.rb', line 238

def count_tokens(
  messages:,
  model:,
  **_provider_options
)
  {
    provider_family: :azure_foundry,
    model: model_id(model),
    supported: false,
    reason: 'Azure AI Foundry REST docs do not define a portable token-counting endpoint for this surface.',
    estimated_input_characters: messages.sum { |message| message.content.to_s.length }
  }
end

#discover_offerings(live: false, raise_on_unreachable: false, **filters) ⇒ Object



308
309
310
311
312
313
314
315
316
317
318
319
# File 'lib/legion/extensions/llm/azure_foundry/provider.rb', line 308

def discover_offerings(live: false, raise_on_unreachable: false, **filters)
  offerings = allowed_offerings
  return filter_offerings(offerings, **filters) unless live

  resolved = offerings.map { |offering| (offering) }
  filter_offerings(resolved, **filters)
rescue Faraday::ConnectionFailed, Faraday::TimeoutError => e
  log.warn("[#{slug}] instance=#{provider_instance_id} unreachable: #{e.message}")
  raise if raise_on_unreachable

  []
end

#embed(text:, model:, **options) ⇒ Object



223
224
225
226
227
228
229
230
231
232
233
234
235
236
# File 'lib/legion/extensions/llm/azure_foundry/provider.rb', line 223

def embed(
  text:,
  model:,
  **options
)
  log.info { "embed request model=#{model}" }
  payload = Utils.deep_merge(
    render_embedding_payload(text, model: model_id(model), dimensions: options[:dimensions]),
    options.fetch(:params, {})
  )
  payload[:input_type] = options[:input_type] if options[:input_type]
  response = connection.post(embedding_url(model:), payload)
  parse_embedding_response(response, model: model_id(model), text:)
end

#embedding_urlObject



150
# File 'lib/legion/extensions/llm/azure_foundry/provider.rb', line 150

def embedding_url(**) = path_for('embeddings')

#headersObject



139
140
141
142
143
144
# File 'lib/legion/extensions/llm/azure_foundry/provider.rb', line 139

def headers
  identity_headers.merge({
    'api-key' => config.azure_foundry_api_key,
    'Authorization' => bearer_header
  }.compact)
end

#health(live: false) ⇒ Object



172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/legion/extensions/llm/azure_foundry/provider.rb', line 172

def health(live: false)
  log.info { "checking health live=#{live} at #{api_base}" }
  baseline = health_baseline(live)
  return baseline.merge(checked: false) unless live

  response = connection.get(health_url)
  baseline.merge(checked: true, ready: true, status: 'healthy', circuit_state: 'closed',
                 raw: response.body)
rescue StandardError => e
  handle_exception(e, level: :warn, handled: true, operation: 'azure_foundry.health')
  baseline.merge(checked: true, ready: false, status: 'unhealthy', circuit_state: 'open',
                 error: e.class.name, message: e.message)
end

#health_urlObject



151
# File 'lib/legion/extensions/llm/azure_foundry/provider.rb', line 151

def health_url = models_url

#list_modelsObject



193
194
195
196
197
198
# File 'lib/legion/extensions/llm/azure_foundry/provider.rb', line 193

def list_models
  log.info { "listing configured deployment models from #{api_base}" }
  models = discover_offerings(live: false).map { |offering| model_info_from_offering(offering) }
  self.class.registry_publisher.publish_models_async(models, readiness: readiness(live: false))
  models
end

#models_urlObject



149
# File 'lib/legion/extensions/llm/azure_foundry/provider.rb', line 149

def models_url = surface == MODEL_INFERENCE_SURFACE ? path_for('info') : path_for('models')

#offering_for(model:, model_family: nil, canonical_model_alias: nil, instance_id: nil, usage_type: nil, **metadata) ⇒ Object

rubocop:disable Metrics/ParameterLists, Metrics/AbcSize



153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/legion/extensions/llm/azure_foundry/provider.rb', line 153

def offering_for(model:, model_family: nil, canonical_model_alias: nil, instance_id: nil, # rubocop:disable Metrics/ParameterLists, Metrics/AbcSize
                 usage_type: nil, **)
  deployment = self.class.deployment_config(model, config:)
  model_id = self.class.resolve_model_id(model, config:)
  configured_family = value_for(deployment, :model_family)
  configured_alias = value_for(deployment, :canonical_model_alias)
  resolved_instance_id = instance_id || provider_instance_id

  build_offering(
    model: model_id,
    instance_id: resolved_instance_id,
    model_family: normalize_family(model_family || configured_family || infer_model_family(model_id)),
    canonical_model_alias: canonical_model_alias || configured_alias,
    usage_type: usage_type || value_for(deployment, :usage_type) || usage_type_for(model_id),
    metadata: .merge((deployment)),
    limits: deployment_limits(deployment)
  )
end

#readiness(live: false) ⇒ Object



186
187
188
189
190
191
# File 'lib/legion/extensions/llm/azure_foundry/provider.rb', line 186

def readiness(live: false)
  log.info { "checking readiness live=#{live} at #{api_base}" }
  health(live: live).merge(local: false, remote: true, endpoints: endpoint_manifest).tap do ||
    self.class.registry_publisher.publish_readiness_async() if live
  end
end

#settingsObject



127
128
129
# File 'lib/legion/extensions/llm/azure_foundry/provider.rb', line 127

def settings
  AzureFoundry.default_settings.dig(:instances, :default)
end

#stream(messages:, model:, **options) ⇒ Object



211
212
213
214
215
216
217
218
219
220
221
# File 'lib/legion/extensions/llm/azure_foundry/provider.rb', line 211

def stream(
  messages:,
  model:,
  **options,
  &
)
  log.info { "stream request model=#{model} messages=#{messages.size}" }
  complete(messages, tools: options.fetch(:tools, {}), temperature: options[:temperature],
                     model: model_info(model, max_tokens: options[:max_tokens]),
                     params: options.fetch(:params, {}), tool_prefs: options[:tool_prefs], &)
end

#stream_urlObject



148
# File 'lib/legion/extensions/llm/azure_foundry/provider.rb', line 148

def stream_url = completion_url

#stream_usage_supported?Boolean

Returns:

  • (Boolean)


125
# File 'lib/legion/extensions/llm/azure_foundry/provider.rb', line 125

def stream_usage_supported? = true