lex-llm-azure-foundry 0.2.7 → 0.2.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +35 -0
- data/Gemfile +0 -7
- data/lex-llm-azure-foundry.gemspec +1 -1
- data/lib/legion/extensions/llm/azure_foundry/actors/discovery_refresh.rb +131 -5
- data/lib/legion/extensions/llm/azure_foundry/provider.rb +118 -28
- data/lib/legion/extensions/llm/azure_foundry/version.rb +1 -1
- data/lib/legion/extensions/llm/azure_foundry.rb +6 -8
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 76e76ee4edefeb764f3d1829a900e1bf60bf9ccc598532feb0d6033680af329f
|
|
4
|
+
data.tar.gz: 778667f3720744c2b5ea38e96d52db29d22c4228f4247a55e9976fd2e1b85612
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f1facb1bad185f1b7d2429213869f637598802d078ea1bf453c670de3ff5426f56e763d0102e3b15ce78f4afb6bd36788bf7269918416191b0daa483e5ae6ca4
|
|
7
|
+
data.tar.gz: aa1a7c88250da3f1d676ef92fbb0f46522d44994e54d49707daad0ec38eb0e940341ea4f9b0661af9d117771b4fe5d3e4156a3c29d21a11b74446dd9fdbd9570
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.13] - 2026-06-20
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- Stub shared registry publishing through `RegistryPublisher#schedule` in specs so async availability-event coverage stays stable after the shared publisher moved off raw `Thread.new`.
|
|
7
|
+
|
|
8
|
+
## [0.2.12] - 2026-06-20
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- Align Azure Foundry instance discovery with the shared `lex-llm` contract by preserving explicit tier overrides while defaulting unconfigured instances to `:cloud`.
|
|
12
|
+
- Restore offline deployment-backed offering discovery and carry the configured provider instance id through Azure offering metadata.
|
|
13
|
+
- Normalize Azure Foundry capability and health metadata to the current shared offering contract.
|
|
14
|
+
|
|
15
|
+
## [0.2.11] - 2026-06-19
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- Adopt `Legion::Extensions::Llm::Inventory::ScopedRefresher` mixin (lex-llm 0.6.0). Discovery
|
|
19
|
+
refresh actors now write directly to the live `Inventory` catalog via `Inventory.write_lane`.
|
|
20
|
+
- Pin `lex-llm >= 0.6.0` and `legion-llm >= 0.14.0` in gemspec.
|
|
21
|
+
- Standard `weight: 100` default added to provider instance settings schema.
|
|
22
|
+
|
|
23
|
+
## 0.2.10 - 2026-06-16
|
|
24
|
+
|
|
25
|
+
- Dependency updates and code quality improvements.
|
|
26
|
+
|
|
27
|
+
## 0.2.9 - 2026-06-15
|
|
28
|
+
|
|
29
|
+
- **CapabilityPolicy integration** — Streaming from `:provider_envelope`; deployment metadata as `:model_metadata`. Settings overrides at provider/instance/model level supported.
|
|
30
|
+
|
|
31
|
+
## 0.2.8 - 2026-06-13
|
|
32
|
+
|
|
33
|
+
- **Gemfile cleanup** — Remove local path overrides; dependencies resolve from gemspec via rubygems.
|
|
34
|
+
- **Dependency bump** — Require `lex-llm >= 0.5.0` for canonical types support.
|
|
35
|
+
- **Capabilities** — Add canonical `:tools` to capability declarations.
|
|
36
|
+
- 26 examples, 0 failures; 13 files, 0 rubocop offenses.
|
|
37
|
+
|
|
3
38
|
## 0.2.7 - 2026-06-02
|
|
4
39
|
|
|
5
40
|
- Add per-provider scoped discovery refresh actor
|
data/Gemfile
CHANGED
|
@@ -2,13 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
source 'https://rubygems.org'
|
|
4
4
|
|
|
5
|
-
group :test do
|
|
6
|
-
llm_base_path = ENV.fetch('LEX_LLM_PATH', File.expand_path('../lex-llm', __dir__))
|
|
7
|
-
transport_path = ENV.fetch('LEGION_TRANSPORT_PATH', File.expand_path('../../legion-transport', __dir__))
|
|
8
|
-
gem 'legion-transport', path: transport_path if File.directory?(transport_path)
|
|
9
|
-
gem 'lex-llm', path: llm_base_path if File.directory?(llm_base_path)
|
|
10
|
-
end
|
|
11
|
-
|
|
12
5
|
gemspec
|
|
13
6
|
|
|
14
7
|
group :development do
|
|
@@ -27,5 +27,5 @@ Gem::Specification.new do |spec|
|
|
|
27
27
|
spec.add_dependency 'legion-logging', '>= 1.3.2'
|
|
28
28
|
spec.add_dependency 'legion-settings', '>= 1.3.14'
|
|
29
29
|
spec.add_dependency 'legion-transport', '>= 1.4.14'
|
|
30
|
-
spec.add_dependency 'lex-llm', '>= 0.
|
|
30
|
+
spec.add_dependency 'lex-llm', '>= 0.6.0'
|
|
31
31
|
end
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'digest'
|
|
4
|
+
|
|
3
5
|
begin
|
|
4
6
|
require 'legion/extensions/actors/every'
|
|
5
7
|
rescue LoadError => e
|
|
6
8
|
warn(e.message) if $VERBOSE
|
|
7
9
|
end
|
|
8
10
|
|
|
11
|
+
begin
|
|
12
|
+
require 'legion/extensions/llm/inventory/scoped_refresher'
|
|
13
|
+
rescue LoadError => e
|
|
14
|
+
warn(e.message) if $VERBOSE
|
|
15
|
+
end
|
|
16
|
+
|
|
9
17
|
return unless defined?(Legion::Extensions::Actors::Every)
|
|
10
18
|
|
|
11
19
|
module Legion
|
|
@@ -13,10 +21,16 @@ module Legion
|
|
|
13
21
|
module Llm
|
|
14
22
|
module AzureFoundry
|
|
15
23
|
module Actor
|
|
16
|
-
class DiscoveryRefresh < Legion::Extensions::Actors::Every # rubocop:disable Style/Documentation
|
|
24
|
+
class DiscoveryRefresh < Legion::Extensions::Actors::Every # rubocop:disable Style/Documentation,Metrics/ClassLength
|
|
17
25
|
include Legion::Logging::Helper
|
|
18
26
|
|
|
19
|
-
|
|
27
|
+
if defined?(Legion::Extensions::Llm::Inventory::ScopedRefresher)
|
|
28
|
+
include Legion::Extensions::Llm::Inventory::ScopedRefresher
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
EMBED_TYPES = %i[embed embedding].freeze
|
|
32
|
+
|
|
33
|
+
def self.every_seconds = 3600
|
|
20
34
|
|
|
21
35
|
def runner_class = self.class
|
|
22
36
|
def runner_function = 'manual'
|
|
@@ -26,19 +40,131 @@ module Legion
|
|
|
26
40
|
def generate_task? = false
|
|
27
41
|
|
|
28
42
|
def time
|
|
29
|
-
return
|
|
43
|
+
return self.class.every_seconds unless defined?(Legion::Settings)
|
|
30
44
|
|
|
31
|
-
Legion::Settings.dig(:extensions, :llm, :azure_foundry, :discovery_interval) ||
|
|
45
|
+
Legion::Settings.dig(:extensions, :llm, :azure_foundry, :discovery_interval) || self.class.every_seconds
|
|
32
46
|
end
|
|
33
47
|
|
|
34
|
-
def manual
|
|
48
|
+
def manual # rubocop:disable Metrics/CyclomaticComplexity
|
|
35
49
|
log.debug('[azure_foundry][discovery_refresh] refreshing model list')
|
|
50
|
+
tick if respond_to?(:tick)
|
|
51
|
+
|
|
36
52
|
return unless defined?(Legion::LLM::Discovery)
|
|
37
53
|
|
|
38
54
|
Legion::LLM::Discovery.refresh_discovered_models!(provider: :azure_foundry)
|
|
55
|
+
|
|
56
|
+
if defined?(Legion::LLM::Router) && Legion::LLM::Router.respond_to?(:populate_auto_rules)
|
|
57
|
+
Legion::LLM::Router.populate_auto_rules(Legion::LLM::Discovery.discovered_instances)
|
|
58
|
+
end
|
|
59
|
+
if defined?(Legion::LLM::Inventory) && Legion::LLM::Inventory.respond_to?(:invalidate_offerings_cache!)
|
|
60
|
+
Legion::LLM::Inventory.invalidate_offerings_cache!
|
|
61
|
+
end
|
|
39
62
|
rescue StandardError => e
|
|
40
63
|
handle_exception(e, level: :warn, handled: true, operation: 'azure_foundry.actor.discovery_refresh')
|
|
41
64
|
end
|
|
65
|
+
|
|
66
|
+
def scope_key(**)
|
|
67
|
+
{ provider: :azure_foundry }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def compute_lanes_for_scope(**)
|
|
71
|
+
return [] unless defined?(Legion::LLM::Call::Registry)
|
|
72
|
+
|
|
73
|
+
instances = Legion::LLM::Call::Registry.all_instances.select do |e|
|
|
74
|
+
(e[:provider] || '').to_sym == :azure_foundry
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
lanes = []
|
|
78
|
+
instances.each { |entry| collect_instance_lanes(entry, lanes) }
|
|
79
|
+
lanes
|
|
80
|
+
rescue StandardError => e
|
|
81
|
+
handle_exception(e, level: :warn, handled: true,
|
|
82
|
+
operation: 'azure_foundry.compute_lanes_for_scope')
|
|
83
|
+
[]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def credential_hash(**)
|
|
87
|
+
settings = Legion::Settings.dig(:extensions, :llm, :azure_foundry) || {}
|
|
88
|
+
::Digest::SHA256.hexdigest(settings[:api_key].to_s + settings[:instances].to_s)[0, 16]
|
|
89
|
+
rescue StandardError
|
|
90
|
+
'unknown'
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def collect_instance_lanes(instance_entry, lanes)
|
|
96
|
+
adapter = instance_entry[:adapter]
|
|
97
|
+
return unless adapter.respond_to?(:discover_offerings)
|
|
98
|
+
|
|
99
|
+
Array(adapter.discover_offerings(live: true)).each do |offering|
|
|
100
|
+
build_and_append_lanes(offering, instance_entry, lanes)
|
|
101
|
+
end
|
|
102
|
+
rescue StandardError => e
|
|
103
|
+
handle_exception(e, level: :warn, handled: true,
|
|
104
|
+
operation: 'azure_foundry.compute_lanes_for_scope.instance')
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def build_and_append_lanes(offering, instance_entry, lanes)
|
|
108
|
+
raw = offering.respond_to?(:to_h) ? offering.to_h : offering
|
|
109
|
+
return unless raw.is_a?(Hash)
|
|
110
|
+
|
|
111
|
+
lane = build_lane(raw, instance_entry)
|
|
112
|
+
lanes << lane
|
|
113
|
+
lanes << lane.merge(id: fleet_id_for(lane), tier: :fleet) if fleet_enabled? && lane[:type] == :inference
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def build_lane(raw, instance_entry) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
117
|
+
model = raw[:model] || raw['model']
|
|
118
|
+
instance_id = raw[:instance_id] || raw['instance_id'] ||
|
|
119
|
+
instance_entry[:instance] || instance_entry[:instance_id] ||
|
|
120
|
+
instance_entry[:id] || :default
|
|
121
|
+
pf = raw[:provider_family] || raw['provider_family'] || :azure_foundry
|
|
122
|
+
type = resolve_type(raw)
|
|
123
|
+
tier = (raw[:tier] || raw['tier'] || :cloud).to_sym
|
|
124
|
+
lane_id = compose_lane_id(tier: tier, provider_family: pf, instance_id: instance_id,
|
|
125
|
+
type: type, model: model)
|
|
126
|
+
|
|
127
|
+
{
|
|
128
|
+
id: lane_id,
|
|
129
|
+
tier: tier,
|
|
130
|
+
provider_family: pf,
|
|
131
|
+
instance_id: instance_id,
|
|
132
|
+
model: model,
|
|
133
|
+
canonical_model_alias: raw[:canonical_model_alias] || raw['canonical_model_alias'],
|
|
134
|
+
type: type,
|
|
135
|
+
capabilities: normalize_capabilities(raw[:capabilities] || raw['capabilities'] || []),
|
|
136
|
+
limits: raw[:limits] || raw['limits'] || {},
|
|
137
|
+
enabled: raw.fetch(:enabled, raw.fetch('enabled', true)),
|
|
138
|
+
cost: raw[:cost] || raw['cost'] || {}
|
|
139
|
+
}
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def resolve_type(raw)
|
|
143
|
+
val = raw[:type] || raw['type'] || raw[:usage_type] || raw['usage_type']
|
|
144
|
+
EMBED_TYPES.include?(val&.to_sym) ? :embedding : :inference
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def normalize_capabilities(raw)
|
|
148
|
+
if defined?(Legion::Extensions::Llm::Inventory::Capabilities)
|
|
149
|
+
Legion::Extensions::Llm::Inventory::Capabilities.normalize(raw)
|
|
150
|
+
else
|
|
151
|
+
Array(raw)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def compose_lane_id(**fields)
|
|
156
|
+
Legion::Extensions::Llm::Inventory::ScopedRefresher.compose_id(fields)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def fleet_id_for(lane)
|
|
160
|
+
compose_lane_id(tier: :fleet, provider_family: lane[:provider_family],
|
|
161
|
+
instance_id: lane[:instance_id], type: lane[:type], model: lane[:model])
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def fleet_enabled?
|
|
165
|
+
settings = Legion::Settings.dig(:extensions, :llm, :azure_foundry) || {}
|
|
166
|
+
settings[:fleet]&.dig(:dispatch, :enabled)
|
|
167
|
+
end
|
|
42
168
|
end
|
|
43
169
|
end
|
|
44
170
|
end
|
|
@@ -121,6 +121,10 @@ module Legion
|
|
|
121
121
|
|
|
122
122
|
def stream_usage_supported? = true
|
|
123
123
|
|
|
124
|
+
def settings
|
|
125
|
+
AzureFoundry.default_settings.dig(:instances, :default)
|
|
126
|
+
end
|
|
127
|
+
|
|
124
128
|
def api_base
|
|
125
129
|
endpoint = config.azure_foundry_endpoint.to_s.sub(%r{/*\z}, '')
|
|
126
130
|
return "#{endpoint}/openai/v1" if surface == OPENAI_V1_SURFACE && !endpoint.end_with?('/openai/v1')
|
|
@@ -143,29 +147,17 @@ module Legion
|
|
|
143
147
|
def embedding_url(**) = path_for('embeddings')
|
|
144
148
|
def health_url = models_url
|
|
145
149
|
|
|
146
|
-
def
|
|
147
|
-
log.info { "discovering offerings live=#{live} from #{api_base}" }
|
|
148
|
-
offerings = filter_offerings(allowed_offerings, **filters)
|
|
149
|
-
return offerings unless live
|
|
150
|
-
|
|
151
|
-
offerings.map do |offering|
|
|
152
|
-
with_live_metadata(offering)
|
|
153
|
-
rescue StandardError => e
|
|
154
|
-
handle_exception(e, level: :warn, handled: true, operation: 'azure_foundry.discover_offerings')
|
|
155
|
-
with_health(offering, ready: false, checked: true, error: e)
|
|
156
|
-
end
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
def offering_for(model:, model_family: nil, canonical_model_alias: nil, instance_id: :default, # rubocop:disable Metrics/ParameterLists
|
|
150
|
+
def offering_for(model:, model_family: nil, canonical_model_alias: nil, instance_id: nil, # rubocop:disable Metrics/ParameterLists, Metrics/AbcSize
|
|
160
151
|
usage_type: nil, **metadata)
|
|
161
152
|
deployment = self.class.deployment_config(model, config:)
|
|
162
153
|
model_id = self.class.resolve_model_id(model, config:)
|
|
163
154
|
configured_family = value_for(deployment, :model_family)
|
|
164
155
|
configured_alias = value_for(deployment, :canonical_model_alias)
|
|
156
|
+
resolved_instance_id = instance_id || provider_instance_id
|
|
165
157
|
|
|
166
158
|
build_offering(
|
|
167
159
|
model: model_id,
|
|
168
|
-
instance_id:
|
|
160
|
+
instance_id: resolved_instance_id,
|
|
169
161
|
model_family: normalize_family(model_family || configured_family || infer_model_family(model_id)),
|
|
170
162
|
canonical_model_alias: canonical_model_alias || configured_alias,
|
|
171
163
|
usage_type: usage_type || value_for(deployment, :usage_type) || usage_type_for(model_id),
|
|
@@ -179,10 +171,12 @@ module Legion
|
|
|
179
171
|
return baseline.merge(checked: false) unless live
|
|
180
172
|
|
|
181
173
|
response = connection.get(health_url)
|
|
182
|
-
baseline.merge(checked: true,
|
|
174
|
+
baseline.merge(checked: true, ready: true, status: 'healthy', circuit_state: 'closed',
|
|
175
|
+
raw: response.body)
|
|
183
176
|
rescue StandardError => e
|
|
184
177
|
handle_exception(e, level: :warn, handled: true, operation: 'azure_foundry.health')
|
|
185
|
-
baseline.merge(checked: true, ready: false,
|
|
178
|
+
baseline.merge(checked: true, ready: false, status: 'unhealthy', circuit_state: 'open',
|
|
179
|
+
error: e.class.name, message: e.message)
|
|
186
180
|
end
|
|
187
181
|
|
|
188
182
|
def readiness(live: false)
|
|
@@ -258,11 +252,15 @@ module Legion
|
|
|
258
252
|
end
|
|
259
253
|
|
|
260
254
|
def health_baseline(live)
|
|
255
|
+
ready = configured?
|
|
261
256
|
{
|
|
262
257
|
provider: :azure_foundry,
|
|
263
|
-
|
|
264
|
-
|
|
258
|
+
instance_id: provider_instance_id,
|
|
259
|
+
configured: ready,
|
|
260
|
+
ready: ready,
|
|
265
261
|
live: live,
|
|
262
|
+
status: ready ? 'healthy' : 'unhealthy',
|
|
263
|
+
circuit_state: ready ? 'closed' : 'open',
|
|
266
264
|
api_base: api_base,
|
|
267
265
|
surface: surface
|
|
268
266
|
}
|
|
@@ -298,6 +296,20 @@ module Legion
|
|
|
298
296
|
token ? "Bearer #{token}" : nil
|
|
299
297
|
end
|
|
300
298
|
|
|
299
|
+
def discover_offerings(live: false, raise_on_unreachable: false, **filters)
|
|
300
|
+
offerings = allowed_offerings
|
|
301
|
+
return filter_offerings(offerings, **filters) unless live
|
|
302
|
+
|
|
303
|
+
resolved = offerings.map { |offering| with_live_metadata(offering) }
|
|
304
|
+
filter_offerings(resolved, **filters)
|
|
305
|
+
rescue Faraday::ConnectionFailed, Faraday::TimeoutError => e
|
|
306
|
+
log.warn("[#{slug}] instance=#{provider_instance_id} unreachable: #{e.message}")
|
|
307
|
+
raise if raise_on_unreachable
|
|
308
|
+
|
|
309
|
+
[]
|
|
310
|
+
end
|
|
311
|
+
public :discover_offerings
|
|
312
|
+
|
|
301
313
|
def configured_deployments
|
|
302
314
|
self.class.normalize_deployments(config.azure_foundry_deployments)
|
|
303
315
|
end
|
|
@@ -315,21 +327,39 @@ module Legion
|
|
|
315
327
|
end
|
|
316
328
|
|
|
317
329
|
def offering_from_config(deployment)
|
|
318
|
-
deployment_name = value_for(deployment, :deployment) || value_for(deployment, :model)
|
|
319
|
-
return nil if deployment_name.to_s.empty?
|
|
320
|
-
|
|
321
330
|
offering_for(
|
|
322
|
-
model:
|
|
331
|
+
model: value_for(deployment, :deployment) || value_for(deployment, :model),
|
|
323
332
|
model_family: value_for(deployment, :model_family),
|
|
324
333
|
canonical_model_alias: value_for(deployment, :canonical_model_alias),
|
|
325
|
-
instance_id:
|
|
326
|
-
usage_type: value_for(deployment, :usage_type),
|
|
327
|
-
|
|
334
|
+
instance_id: provider_instance_id,
|
|
335
|
+
usage_type: value_for(deployment, :usage_type) || :inference,
|
|
336
|
+
metadata: deployment_metadata(deployment)
|
|
337
|
+
)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def offering_from_model(model_info, _health: nil) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
341
|
+
model_id = model_info.respond_to?(:id) ? model_info.id : model_info['id']
|
|
342
|
+
name = model_info.respond_to?(:name) ? model_info.name : model_info['name'] || model_id
|
|
343
|
+
family = model_info.respond_to?(:family) ? model_info.family : model_info['model_family']
|
|
344
|
+
usage = model_info.respond_to?(:embedding?) && model_info.embedding? ? :embedding : :inference
|
|
345
|
+
meta = if model_info.respond_to?(:metadata)
|
|
346
|
+
model_info.metadata
|
|
347
|
+
else
|
|
348
|
+
model_info.is_a?(Hash) ? model_info : {}
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
offering_for(
|
|
352
|
+
model: model_id,
|
|
353
|
+
model_family: family,
|
|
354
|
+
canonical_model_alias: name,
|
|
355
|
+
instance_id: provider_instance_id,
|
|
356
|
+
usage_type: usage,
|
|
357
|
+
metadata: meta
|
|
328
358
|
)
|
|
329
359
|
end
|
|
330
360
|
|
|
331
361
|
def build_offering(model:, model_family:, usage_type:, instance_id:, canonical_model_alias:, metadata:) # rubocop:disable Metrics/ParameterLists
|
|
332
|
-
|
|
362
|
+
policy = resolve_capability_policy(model, usage_type)
|
|
333
363
|
Legion::Extensions::Llm::Routing::ModelOffering.new(
|
|
334
364
|
provider_family: :azure_foundry,
|
|
335
365
|
instance_id: instance_id,
|
|
@@ -337,7 +367,8 @@ module Legion
|
|
|
337
367
|
tier: offering_tier,
|
|
338
368
|
model: model,
|
|
339
369
|
usage_type: usage_type.to_sym,
|
|
340
|
-
capabilities: capabilities,
|
|
370
|
+
capabilities: policy[:capabilities],
|
|
371
|
+
capability_sources: policy[:sources],
|
|
341
372
|
metadata: metadata.merge(
|
|
342
373
|
model_family: model_family,
|
|
343
374
|
canonical_model_alias: canonical_model_alias,
|
|
@@ -373,6 +404,65 @@ module Legion
|
|
|
373
404
|
deployment.to_h.transform_keys(&:to_sym).except(:deployment, :model_family, :usage_type)
|
|
374
405
|
end
|
|
375
406
|
|
|
407
|
+
def resolve_capability_policy(model, usage_type)
|
|
408
|
+
if usage_type.to_sym == :embedding
|
|
409
|
+
return { capabilities: %i[embedding], sources: { embedding: { value: true, source: :model_metadata } } }
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
real_caps = real_capabilities_for(model)
|
|
413
|
+
provider_cfg = provider_level_config
|
|
414
|
+
instance_cfg = instance_level_config
|
|
415
|
+
model_cfg = model_config_for(model)
|
|
416
|
+
|
|
417
|
+
Legion::Extensions::Llm::CapabilityPolicy.resolve(
|
|
418
|
+
real: real_caps,
|
|
419
|
+
provider_catalog: {},
|
|
420
|
+
probe: {},
|
|
421
|
+
provider_envelope: { streaming: true },
|
|
422
|
+
provider_config: provider_cfg,
|
|
423
|
+
instance_config: instance_cfg,
|
|
424
|
+
model_config: model_cfg
|
|
425
|
+
)
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def real_capabilities_for(model)
|
|
429
|
+
caps = {}
|
|
430
|
+
caps[:streaming] = true if Capabilities.streaming?(model)
|
|
431
|
+
caps[:tools] = true if Capabilities.functions?(model)
|
|
432
|
+
caps[:vision] = true if Capabilities.vision?(model)
|
|
433
|
+
caps
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def instance_level_config
|
|
437
|
+
if config.respond_to?(:to_h)
|
|
438
|
+
config.to_h
|
|
439
|
+
elsif config.respond_to?(:instance_variable_get)
|
|
440
|
+
data = config.instance_variable_get(:@data)
|
|
441
|
+
data.is_a?(Hash) ? data : {}
|
|
442
|
+
else
|
|
443
|
+
{}
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def provider_level_config
|
|
448
|
+
cfg = Legion::Extensions::Llm::CredentialSources.setting(:extensions, :llm, :azure_foundry)
|
|
449
|
+
return {} unless cfg.is_a?(Hash)
|
|
450
|
+
|
|
451
|
+
cfg.except(:instances, 'instances')
|
|
452
|
+
rescue StandardError => e
|
|
453
|
+
handle_exception(e, level: :warn, handled: true, operation: 'azure_foundry.provider_level_config')
|
|
454
|
+
{}
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
def model_config_for(model)
|
|
458
|
+
provider_cfg = provider_level_config
|
|
459
|
+
models = provider_cfg[:models] || provider_cfg['models']
|
|
460
|
+
return {} unless models.is_a?(Hash)
|
|
461
|
+
|
|
462
|
+
model_id_str = Capabilities.model_id(model)
|
|
463
|
+
models[model_id_str.to_sym] || models[model_id_str] || {}
|
|
464
|
+
end
|
|
465
|
+
|
|
376
466
|
def capabilities_for(model, usage_type)
|
|
377
467
|
return %i[embedding] if usage_type.to_sym == :embedding
|
|
378
468
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'legion/extensions/llm'
|
|
4
4
|
require 'legion/extensions/llm/azure_foundry/provider'
|
|
5
5
|
require 'legion/extensions/llm/azure_foundry/version'
|
|
6
|
+
require_relative 'azure_foundry/actors/discovery_refresh'
|
|
6
7
|
|
|
7
8
|
module Legion
|
|
8
9
|
module Extensions
|
|
@@ -36,10 +37,7 @@ module Legion
|
|
|
36
37
|
fleet: {
|
|
37
38
|
enabled: false,
|
|
38
39
|
respond_to_requests: false,
|
|
39
|
-
capabilities: %i[chat stream_chat embed]
|
|
40
|
-
lanes: [],
|
|
41
|
-
concurrency: 4,
|
|
42
|
-
queue_suffix: nil
|
|
40
|
+
capabilities: %i[chat stream_chat embed tools]
|
|
43
41
|
}
|
|
44
42
|
}
|
|
45
43
|
)
|
|
@@ -68,7 +66,7 @@ module Legion
|
|
|
68
66
|
cfg['api_base']
|
|
69
67
|
return if endpoint.nil? || endpoint.to_s.strip.empty?
|
|
70
68
|
|
|
71
|
-
instances[:settings] = normalize_instance_config(cfg).merge(tier: :cloud)
|
|
69
|
+
instances[:settings] = normalize_instance_config(cfg).merge(tier: cfg[:tier] || cfg['tier'] || :cloud)
|
|
72
70
|
end
|
|
73
71
|
|
|
74
72
|
def self.discover_named_instances(instances)
|
|
@@ -88,7 +86,8 @@ module Legion
|
|
|
88
86
|
config[:api_base] || config['api_base']
|
|
89
87
|
return if endpoint.nil? || endpoint.to_s.strip.empty?
|
|
90
88
|
|
|
91
|
-
instances[name.to_sym] =
|
|
89
|
+
instances[name.to_sym] =
|
|
90
|
+
normalize_instance_config(config).merge(tier: config[:tier] || config['tier'] || :cloud)
|
|
92
91
|
end
|
|
93
92
|
|
|
94
93
|
def self.normalize_instance_config(config) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
@@ -107,8 +106,7 @@ module Legion
|
|
|
107
106
|
private_class_method :discover_default_instance, :discover_named_instances, :add_named_instance,
|
|
108
107
|
:normalize_instance_config
|
|
109
108
|
|
|
110
|
-
Legion::Extensions::Llm::Configuration.register_provider_options(Provider.configuration_options)
|
|
111
|
-
Legion::Extensions::Llm::Configuration.respond_to?(:register_provider_options)
|
|
109
|
+
Legion::Extensions::Llm::Configuration.register_provider_options(Provider.configuration_options)
|
|
112
110
|
end
|
|
113
111
|
end
|
|
114
112
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lex-llm-azure-foundry
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.13
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- LegionIO
|
|
@@ -71,14 +71,14 @@ dependencies:
|
|
|
71
71
|
requirements:
|
|
72
72
|
- - ">="
|
|
73
73
|
- !ruby/object:Gem::Version
|
|
74
|
-
version: 0.
|
|
74
|
+
version: 0.6.0
|
|
75
75
|
type: :runtime
|
|
76
76
|
prerelease: false
|
|
77
77
|
version_requirements: !ruby/object:Gem::Requirement
|
|
78
78
|
requirements:
|
|
79
79
|
- - ">="
|
|
80
80
|
- !ruby/object:Gem::Version
|
|
81
|
-
version: 0.
|
|
81
|
+
version: 0.6.0
|
|
82
82
|
description: Azure AI Foundry and Azure OpenAI hosted provider integration for LegionIO
|
|
83
83
|
LLM routing.
|
|
84
84
|
email:
|