lex-llm-azure-foundry 0.1.4 → 0.1.5
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 +8 -0
- data/README.md +45 -5
- data/lib/legion/extensions/llm/azure_foundry/provider.rb +21 -8
- data/lib/legion/extensions/llm/azure_foundry/registry_event_builder.rb +8 -0
- data/lib/legion/extensions/llm/azure_foundry/registry_publisher.rb +12 -17
- data/lib/legion/extensions/llm/azure_foundry/version.rb +1 -1
- data/lib/legion/extensions/llm/azure_foundry.rb +1 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 93b2b78c6462da9e924d9de921741cc21a406b68d47879a23c21d26df0478e88
|
|
4
|
+
data.tar.gz: a91ed47ace39e1a38db76d35a0475fb9332a60213cbf2bd4816a31fe988c58a6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 58ec3150d974660d8f573f282fbafa1c2d5716c817453da7124fa2f1125852dacac0d388e34e406535a43178da29c20e3b5ccd00f54d39c3581bde2bc949cd19
|
|
7
|
+
data.tar.gz: 1f53bccbe6444087c3269de7ad8d1a82ee00681dd4bf0670477b144dfb4994fe3a9f22c1738e067d38c0dcd4e1fe011ca243d6095adbeb28dc50ec9480d4a693
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.5] - 2026-04-30
|
|
4
|
+
|
|
5
|
+
- Audit all rescue blocks for handle_exception compliance across Provider, RegistryPublisher, and RegistryEventBuilder
|
|
6
|
+
- Add Legion::Logging::Helper to AzureFoundry module, RegistryPublisher, and RegistryEventBuilder
|
|
7
|
+
- Add info-level action logging for discover_offerings, health, readiness, list_models, chat, stream, embed, and registry publish
|
|
8
|
+
- Remove custom log_publish_failure in favour of standard handle_exception
|
|
9
|
+
- Update README to reflect current architecture, file map, and observability
|
|
10
|
+
|
|
3
11
|
## [0.1.4] - 2026-04-30
|
|
4
12
|
|
|
5
13
|
- Enable stream_usage_supported? for streaming token usage reporting
|
data/README.md
CHANGED
|
@@ -13,11 +13,49 @@ Load it with `require 'legion/extensions/llm/azure_foundry'`.
|
|
|
13
13
|
- Azure AI Foundry model inference embeddings through `POST /models/embeddings?api-version=...`
|
|
14
14
|
- Azure AI Foundry model info health check through `GET /models/info?api-version=...` when `live: true`
|
|
15
15
|
- Azure OpenAI v1-compatible endpoint support through `/openai/v1/chat/completions` and `/openai/v1/embeddings`
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
16
|
+
- Deployment-name-preserving routing offerings for hosted Azure deployments
|
|
17
|
+
- Explicit `model_family` and `canonical_model_alias` metadata for deployments whose base model cannot be proven from Azure metadata
|
|
18
|
+
- Offline-first discovery from configured deployments
|
|
19
|
+
- Shared OpenAI-compatible request and response mapping via `Legion::Extensions::Llm::Provider::OpenAICompatible`
|
|
20
|
+
- Conservative token-counting metadata when no portable Azure token-counting REST endpoint is configured
|
|
21
|
+
- Best-effort `llm.registry` event publishing for readiness and model availability via AMQP when transport is available
|
|
22
|
+
|
|
23
|
+
## Architecture
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
Legion::Extensions::Llm::AzureFoundry
|
|
27
|
+
├── Provider # Azure AI Foundry and Azure OpenAI hosted provider surface
|
|
28
|
+
│ └── Capabilities # Capability predicates inferred from deployment metadata and model naming
|
|
29
|
+
├── RegistryPublisher # Best-effort async publisher for llm.registry availability events
|
|
30
|
+
├── RegistryEventBuilder # Builds sanitized lex-llm registry envelopes for provider state
|
|
31
|
+
├── Transport/
|
|
32
|
+
│ ├── Messages::RegistryEvent # AMQP message for llm.registry events
|
|
33
|
+
│ └── Exchanges::LlmRegistry # Topic exchange for provider availability events
|
|
34
|
+
└── VERSION
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## File Map
|
|
38
|
+
|
|
39
|
+
| Path | Purpose |
|
|
40
|
+
|------|---------|
|
|
41
|
+
| `lib/legion/extensions/llm/azure_foundry.rb` | Entry point, provider registration, default settings |
|
|
42
|
+
| `lib/legion/extensions/llm/azure_foundry/provider.rb` | Provider implementation with chat, stream, embed, health, readiness, discovery |
|
|
43
|
+
| `lib/legion/extensions/llm/azure_foundry/registry_publisher.rb` | Async registry event publishing with transport guards |
|
|
44
|
+
| `lib/legion/extensions/llm/azure_foundry/registry_event_builder.rb` | Sanitized registry envelope construction |
|
|
45
|
+
| `lib/legion/extensions/llm/azure_foundry/transport/messages/registry_event.rb` | AMQP message class for registry events |
|
|
46
|
+
| `lib/legion/extensions/llm/azure_foundry/transport/exchanges/llm_registry.rb` | Topic exchange definition for llm.registry |
|
|
47
|
+
| `lib/legion/extensions/llm/azure_foundry/version.rb` | `VERSION` constant |
|
|
48
|
+
|
|
49
|
+
## Observability
|
|
50
|
+
|
|
51
|
+
Every class and module uses `Legion::Logging::Helper`:
|
|
52
|
+
|
|
53
|
+
- **AzureFoundry** module: `extend Legion::Logging::Helper`
|
|
54
|
+
- **Provider**: inherits `include Legion::Logging::Helper` from `Legion::Extensions::Llm::Provider`
|
|
55
|
+
- **RegistryPublisher**: `include Legion::Logging::Helper`
|
|
56
|
+
- **RegistryEventBuilder**: `include Legion::Logging::Helper`
|
|
57
|
+
|
|
58
|
+
All rescue blocks call `handle_exception(e, level:, handled:, operation:)` for structured exception reporting. Key actions emit info-level log lines including discover_offerings, health checks, readiness, model listing, chat, stream, embed, and registry publish operations.
|
|
21
59
|
|
|
22
60
|
## API Contract
|
|
23
61
|
|
|
@@ -97,6 +135,8 @@ provider = Legion::Extensions::Llm::AzureFoundry.provider_class.new(Legion::Exte
|
|
|
97
135
|
provider.discover_offerings(live: false)
|
|
98
136
|
provider.offering_for(model: "gpt-4o-prod", model_family: :openai, canonical_model_alias: "gpt-4o")
|
|
99
137
|
provider.health(live: false)
|
|
138
|
+
provider.readiness(live: false)
|
|
139
|
+
provider.list_models
|
|
100
140
|
provider.chat(messages, model: "gpt-4o-prod")
|
|
101
141
|
provider.stream(messages, model: "gpt-4o-prod") { |chunk| puts chunk.content }
|
|
102
142
|
provider.embed(["hello"], model: "embedding-prod")
|
|
@@ -144,12 +144,14 @@ module Legion
|
|
|
144
144
|
def health_url = models_url
|
|
145
145
|
|
|
146
146
|
def discover_offerings(live: false, **filters)
|
|
147
|
+
log.info { "discovering offerings live=#{live} from #{api_base}" }
|
|
147
148
|
offerings = configured_deployments.filter_map { |deployment| offering_from_config(deployment) }
|
|
148
149
|
return filter_offerings(offerings, **filters) unless live
|
|
149
150
|
|
|
150
151
|
filter_offerings(offerings, **filters).map do |offering|
|
|
151
152
|
with_live_metadata(offering)
|
|
152
153
|
rescue StandardError => e
|
|
154
|
+
handle_exception(e, level: :warn, handled: true, operation: 'azure_foundry.discover_offerings')
|
|
153
155
|
with_health(offering, ready: false, checked: true, error: e)
|
|
154
156
|
end
|
|
155
157
|
end
|
|
@@ -172,43 +174,43 @@ module Legion
|
|
|
172
174
|
end
|
|
173
175
|
|
|
174
176
|
def health(live: false)
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
configured: configured?,
|
|
178
|
-
ready: configured?,
|
|
179
|
-
live: live,
|
|
180
|
-
api_base: api_base,
|
|
181
|
-
surface: surface
|
|
182
|
-
}
|
|
177
|
+
log.info { "checking health live=#{live} at #{api_base}" }
|
|
178
|
+
baseline = health_baseline(live)
|
|
183
179
|
return baseline.merge(checked: false) unless live
|
|
184
180
|
|
|
185
181
|
response = connection.get(health_url)
|
|
186
182
|
baseline.merge(checked: true, model_info: response.body)
|
|
187
183
|
rescue StandardError => e
|
|
184
|
+
handle_exception(e, level: :warn, handled: true, operation: 'azure_foundry.health')
|
|
188
185
|
baseline.merge(checked: true, ready: false, error: e.class.name, message: e.message)
|
|
189
186
|
end
|
|
190
187
|
|
|
191
188
|
def readiness(live: false)
|
|
189
|
+
log.info { "checking readiness live=#{live} at #{api_base}" }
|
|
192
190
|
health(live: live).merge(local: false, remote: true, endpoints: endpoint_manifest).tap do |metadata|
|
|
193
191
|
self.class.registry_publisher.publish_readiness_async(metadata) if live
|
|
194
192
|
end
|
|
195
193
|
end
|
|
196
194
|
|
|
197
195
|
def list_models
|
|
196
|
+
log.info { "listing configured deployment models from #{api_base}" }
|
|
198
197
|
models = discover_offerings(live: false).map { |offering| model_info_from_offering(offering) }
|
|
199
198
|
self.class.registry_publisher.publish_models_async(models, readiness: readiness(live: false))
|
|
200
199
|
models
|
|
201
200
|
end
|
|
202
201
|
|
|
203
202
|
def chat(messages, model:, temperature: nil, max_tokens: nil, tools: {}, tool_prefs: nil, params: {}) # rubocop:disable Metrics/ParameterLists
|
|
203
|
+
log.info { "chat request model=#{model} messages=#{messages.size}" }
|
|
204
204
|
complete(messages, tools:, temperature:, model: model_info(model, max_tokens:), params:, tool_prefs:)
|
|
205
205
|
end
|
|
206
206
|
|
|
207
207
|
def stream(messages, model:, temperature: nil, max_tokens: nil, tools: {}, tool_prefs: nil, params: {}, &) # rubocop:disable Metrics/ParameterLists
|
|
208
|
+
log.info { "stream request model=#{model} messages=#{messages.size}" }
|
|
208
209
|
complete(messages, tools:, temperature:, model: model_info(model, max_tokens:), params:, tool_prefs:, &)
|
|
209
210
|
end
|
|
210
211
|
|
|
211
212
|
def embed(text, model:, dimensions: nil, input_type: nil)
|
|
213
|
+
log.info { "embed request model=#{model}" }
|
|
212
214
|
payload = render_embedding_payload(text, model: model_id(model), dimensions:)
|
|
213
215
|
payload[:input_type] = input_type if input_type
|
|
214
216
|
response = connection.post(embedding_url(model:), payload)
|
|
@@ -231,6 +233,17 @@ module Legion
|
|
|
231
233
|
(config.azure_foundry_surface || MODEL_INFERENCE_SURFACE).to_sym
|
|
232
234
|
end
|
|
233
235
|
|
|
236
|
+
def health_baseline(live)
|
|
237
|
+
{
|
|
238
|
+
provider: :azure_foundry,
|
|
239
|
+
configured: configured?,
|
|
240
|
+
ready: configured?,
|
|
241
|
+
live: live,
|
|
242
|
+
api_base: api_base,
|
|
243
|
+
surface: surface
|
|
244
|
+
}
|
|
245
|
+
end
|
|
246
|
+
|
|
234
247
|
def model_info_from_offering(offering)
|
|
235
248
|
capabilities = offering.capabilities.map(&:to_s)
|
|
236
249
|
Legion::Extensions::Llm::Model::Info.new(
|
|
@@ -6,6 +6,8 @@ module Legion
|
|
|
6
6
|
module AzureFoundry
|
|
7
7
|
# Builds sanitized lex-llm registry envelopes for Azure Foundry provider state.
|
|
8
8
|
class RegistryEventBuilder
|
|
9
|
+
include Legion::Logging::Helper
|
|
10
|
+
|
|
9
11
|
def readiness(readiness)
|
|
10
12
|
registry_event_class.public_send(
|
|
11
13
|
readiness[:ready] ? :available : :unavailable,
|
|
@@ -108,6 +110,12 @@ module Legion
|
|
|
108
110
|
end
|
|
109
111
|
|
|
110
112
|
def provider_instance
|
|
113
|
+
configured_node = (::Legion::Settings.dig(:node, :canonical_name) if defined?(::Legion::Settings))
|
|
114
|
+
value = configured_node.to_s.strip
|
|
115
|
+
value.empty? ? :azure_foundry : value.to_sym
|
|
116
|
+
rescue StandardError => e
|
|
117
|
+
handle_exception(e, level: :debug, handled: true,
|
|
118
|
+
operation: 'azure_foundry.registry.provider_instance')
|
|
111
119
|
:azure_foundry
|
|
112
120
|
end
|
|
113
121
|
|
|
@@ -6,6 +6,8 @@ module Legion
|
|
|
6
6
|
module AzureFoundry
|
|
7
7
|
# Best-effort publisher for Azure Foundry provider availability events.
|
|
8
8
|
class RegistryPublisher
|
|
9
|
+
include Legion::Logging::Helper
|
|
10
|
+
|
|
9
11
|
APP_ID = 'lex-llm-azure-foundry'
|
|
10
12
|
|
|
11
13
|
def initialize(builder: RegistryEventBuilder.new)
|
|
@@ -13,10 +15,12 @@ module Legion
|
|
|
13
15
|
end
|
|
14
16
|
|
|
15
17
|
def publish_readiness_async(readiness)
|
|
18
|
+
log.info { 'publishing readiness event to llm.registry' }
|
|
16
19
|
schedule { publish_event(@builder.readiness(readiness)) }
|
|
17
20
|
end
|
|
18
21
|
|
|
19
22
|
def publish_models_async(models, readiness:)
|
|
23
|
+
log.info { "publishing #{Array(models).size} model event(s) to llm.registry" }
|
|
20
24
|
schedule do
|
|
21
25
|
Array(models).each do |model|
|
|
22
26
|
publish_event(@builder.model_available(model, readiness:))
|
|
@@ -33,10 +37,10 @@ module Legion
|
|
|
33
37
|
Thread.current.abort_on_exception = false
|
|
34
38
|
yield
|
|
35
39
|
rescue StandardError => e
|
|
36
|
-
|
|
40
|
+
handle_exception(e, level: :debug, handled: true, operation: 'azure_foundry.registry.schedule_thread')
|
|
37
41
|
end
|
|
38
42
|
rescue StandardError => e
|
|
39
|
-
|
|
43
|
+
handle_exception(e, level: :debug, handled: true, operation: 'azure_foundry.registry.schedule')
|
|
40
44
|
false
|
|
41
45
|
end
|
|
42
46
|
|
|
@@ -45,7 +49,7 @@ module Legion
|
|
|
45
49
|
|
|
46
50
|
message_class.new(event:, app_id: APP_ID).publish(spool: false)
|
|
47
51
|
rescue StandardError => e
|
|
48
|
-
|
|
52
|
+
handle_exception(e, level: :warn, handled: true, operation: 'azure_foundry.registry.publish_event')
|
|
49
53
|
false
|
|
50
54
|
end
|
|
51
55
|
|
|
@@ -56,7 +60,9 @@ module Legion
|
|
|
56
60
|
return true unless ::Legion::Transport::Connection.respond_to?(:session_open?)
|
|
57
61
|
|
|
58
62
|
::Legion::Transport::Connection.session_open?
|
|
59
|
-
rescue StandardError
|
|
63
|
+
rescue StandardError => e
|
|
64
|
+
handle_exception(e, level: :debug, handled: true,
|
|
65
|
+
operation: 'azure_foundry.registry.publishing_available?')
|
|
60
66
|
false
|
|
61
67
|
end
|
|
62
68
|
|
|
@@ -70,7 +76,8 @@ module Legion
|
|
|
70
76
|
|
|
71
77
|
require 'legion/extensions/llm/azure_foundry/transport/messages/registry_event'
|
|
72
78
|
message_class_defined?
|
|
73
|
-
rescue LoadError
|
|
79
|
+
rescue LoadError => e
|
|
80
|
+
handle_exception(e, level: :debug, handled: true, operation: 'azure_foundry.registry.transport_load')
|
|
74
81
|
false
|
|
75
82
|
end
|
|
76
83
|
|
|
@@ -81,18 +88,6 @@ module Legion
|
|
|
81
88
|
def message_class
|
|
82
89
|
::Legion::Extensions::Llm::AzureFoundry::Transport::Messages::RegistryEvent
|
|
83
90
|
end
|
|
84
|
-
|
|
85
|
-
def log_publish_failure(error, level: :warn)
|
|
86
|
-
message = "[lex-llm-azure-foundry] llm.registry publish failed: #{error.class}: #{error.message}"
|
|
87
|
-
logger = ::Legion::Extensions::Llm.logger if defined?(::Legion::Extensions::Llm)
|
|
88
|
-
if logger.respond_to?(level)
|
|
89
|
-
logger.public_send(level, message)
|
|
90
|
-
elsif logger.respond_to?(:debug)
|
|
91
|
-
logger.debug(message)
|
|
92
|
-
end
|
|
93
|
-
rescue StandardError
|
|
94
|
-
nil
|
|
95
|
-
end
|
|
96
91
|
end
|
|
97
92
|
end
|
|
98
93
|
end
|