lex-llm-openai 0.1.7 → 0.2.0
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/.rubocop.yml +4 -0
- data/CHANGELOG.md +16 -0
- data/README.md +61 -13
- data/lex-llm-openai.gemspec +1 -1
- data/lib/legion/extensions/llm/openai/provider.rb +136 -4
- data/lib/legion/extensions/llm/openai/version.rb +1 -1
- data/lib/legion/extensions/llm/openai.rb +13 -16
- metadata +3 -7
- data/lib/legion/extensions/llm/openai/registry_event_builder.rb +0 -72
- data/lib/legion/extensions/llm/openai/registry_publisher.rb +0 -96
- data/lib/legion/extensions/llm/openai/transport/exchanges/llm_registry.rb +0 -24
- data/lib/legion/extensions/llm/openai/transport/messages/registry_event.rb +0 -42
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3adf2416815789eee25cb13dc0ceaadda14549fa6f897ea8c0216a5ebd728793
|
|
4
|
+
data.tar.gz: 317ad6bcc34b252a9026e9aaae5a9a6fe8f850ab67c7e50e62f67a5c43d3538f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6e5b1684b8a9a328278bb0ab039940f924f43943847fe45d0b485c82702f4cbb426b142e751d407ed736778b9923bfffd8e8b323549bef335c99056f989c4250
|
|
7
|
+
data.tar.gz: 0ff1be0c6ccd2fa4e24a3c9bb8dc816e6dc249ff6a88177240be0f5fc4aedade94cb9955d752f28570af099a64e2dea232e3d7bdef0587ab7ef8a308dbe23684
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.0] - 2026-04-30
|
|
4
|
+
- **BREAKING**: Adopt base contract from lex-llm 0.1.9; require `lex-llm >= 0.1.9`
|
|
5
|
+
- Replace `provider_settings`-based `default_settings` with flat provider defaults (enabled, default_model, api_key, etc.)
|
|
6
|
+
- Remove deprecated `Provider.register` call; configuration options are now registered at class-load time
|
|
7
|
+
- Delete local `RegistryPublisher` and `RegistryEventBuilder`; use parameterized base classes from lex-llm
|
|
8
|
+
- Delete local `transport/` directory (exchanges, messages); use shared transport from lex-llm
|
|
9
|
+
- Add static `CAPABILITY_MAP` for known OpenAI model families; `list_models` now returns `Model::Info` structs directly
|
|
10
|
+
- `list_models` no longer delegates to `parse_list_models_response`; builds `Model::Info` via the static capability map
|
|
11
|
+
|
|
12
|
+
## [0.1.8] - 2026-04-30
|
|
13
|
+
- Add Legion::Logging::Helper to all modules and classes for structured observability
|
|
14
|
+
- Replace bare rescue blocks with handle_exception for unified error telemetry
|
|
15
|
+
- Add info-level action logging for provider registration, model listing, model retrieval, and registry publishing
|
|
16
|
+
- Remove manual log_publish_failure helper in favor of handle_exception
|
|
17
|
+
- Update README to reflect current capabilities and architecture
|
|
18
|
+
|
|
3
19
|
## [0.1.7] - 2026-04-30
|
|
4
20
|
- Enable stream_usage_supported? for streaming token usage reporting
|
|
5
21
|
|
data/README.md
CHANGED
|
@@ -9,19 +9,42 @@ Load it with `require 'legion/extensions/llm/openai'`.
|
|
|
9
9
|
## What It Provides
|
|
10
10
|
|
|
11
11
|
- `Legion::Extensions::Llm::Provider` registration as `:openai`
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
12
|
+
- Chat completions via `POST /v1/chat/completions`
|
|
13
|
+
- Streaming chat completions (same endpoint, `stream: true`)
|
|
14
|
+
- Model discovery via `GET /v1/models`
|
|
15
|
+
- Model retrieval via `GET /v1/models/{model}`
|
|
16
|
+
- Embeddings via `POST /v1/embeddings`
|
|
17
|
+
- Moderation via `POST /v1/moderations`
|
|
18
|
+
- Image generation via `POST /v1/images/generations`
|
|
19
|
+
- Image editing via `POST /v1/images/edits`
|
|
20
|
+
- Image variation via `POST /v1/images/variations`
|
|
21
|
+
- Audio transcription via `POST /v1/audio/transcriptions`
|
|
22
|
+
- Streaming token usage reporting (`stream_usage_supported?`)
|
|
23
|
+
- Shared OpenAI-compatible request/response mapping via `Legion::Extensions::Llm::Provider::OpenAICompatible`
|
|
24
|
+
- Normalized chat, embedding, moderation, image, and audio capability mapping for discovered models
|
|
25
|
+
- Shared fleet/default settings via `Legion::Extensions::Llm.provider_settings`
|
|
26
|
+
- Best-effort `llm.registry` availability event publishing for discovered models
|
|
27
|
+
|
|
28
|
+
## Architecture
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
Legion::Extensions::Llm::Openai
|
|
32
|
+
├── Provider # OpenAI provider implementation (chat, models, embeddings, etc.)
|
|
33
|
+
│ └── Capabilities # Model family capability predicates
|
|
34
|
+
├── RegistryPublisher # Async llm.registry event publisher
|
|
35
|
+
├── RegistryEventBuilder # Sanitized registry event envelope builder
|
|
36
|
+
└── Transport
|
|
37
|
+
├── Exchanges::LlmRegistry # Topic exchange for llm.registry
|
|
38
|
+
└── Messages::RegistryEvent # AMQP message for registry events
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Observability
|
|
42
|
+
|
|
43
|
+
All classes include `Legion::Logging::Helper` (when available) providing:
|
|
44
|
+
|
|
45
|
+
- Structured `handle_exception` calls on every rescue block
|
|
46
|
+
- Info-level action logging for provider registration, model listing, model retrieval, and registry publishing
|
|
47
|
+
- Automatic log segment derivation and component type tagging
|
|
25
48
|
|
|
26
49
|
## Defaults
|
|
27
50
|
|
|
@@ -47,6 +70,9 @@ Legion::Extensions::Llm::Openai.default_settings
|
|
|
47
70
|
```ruby
|
|
48
71
|
Legion::Extensions::Llm.configure do |config|
|
|
49
72
|
config.openai_api_key = ENV.fetch("OPENAI_API_KEY")
|
|
73
|
+
config.openai_api_base = nil # defaults to https://api.openai.com
|
|
74
|
+
config.openai_organization_id = nil # optional OpenAI-Organization header
|
|
75
|
+
config.openai_project_id = nil # optional OpenAI-Project header
|
|
50
76
|
config.default_model = "gpt-5.2"
|
|
51
77
|
config.default_embedding_model = "text-embedding-3-small"
|
|
52
78
|
config.default_moderation_model = "omni-moderation-latest"
|
|
@@ -54,3 +80,25 @@ Legion::Extensions::Llm.configure do |config|
|
|
|
54
80
|
config.default_transcription_model = "gpt-4o-transcribe"
|
|
55
81
|
end
|
|
56
82
|
```
|
|
83
|
+
|
|
84
|
+
## Dependencies
|
|
85
|
+
|
|
86
|
+
| Gem | Purpose |
|
|
87
|
+
|-----|---------|
|
|
88
|
+
| `lex-llm` (>= 0.1.5) | Shared provider contract, fleet settings, routing |
|
|
89
|
+
| `legion-json` (>= 1.2.1) | JSON serialization |
|
|
90
|
+
| `legion-logging` (>= 1.3.2) | Structured logging via Helper |
|
|
91
|
+
| `legion-settings` (>= 1.3.14) | Configuration management |
|
|
92
|
+
|
|
93
|
+
## Development
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
bundle install
|
|
97
|
+
bundle exec rspec --format progress # all pass
|
|
98
|
+
bundle exec rubocop -A # auto-fix
|
|
99
|
+
bundle exec rubocop # lint check (0 offenses)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## License
|
|
103
|
+
|
|
104
|
+
MIT
|
data/lex-llm-openai.gemspec
CHANGED
|
@@ -26,5 +26,5 @@ Gem::Specification.new do |spec|
|
|
|
26
26
|
spec.add_dependency 'legion-json', '>= 1.2.1'
|
|
27
27
|
spec.add_dependency 'legion-logging', '>= 1.3.2'
|
|
28
28
|
spec.add_dependency 'legion-settings', '>= 1.3.14'
|
|
29
|
-
spec.add_dependency 'lex-llm', '>= 0.
|
|
29
|
+
spec.add_dependency 'lex-llm', '>= 0.2.0'
|
|
30
30
|
end
|
|
@@ -9,6 +9,84 @@ module Legion
|
|
|
9
9
|
# OpenAI provider implementation for the Legion::Extensions::Llm base provider contract.
|
|
10
10
|
class Provider < Legion::Extensions::Llm::Provider
|
|
11
11
|
include Legion::Extensions::Llm::Provider::OpenAICompatible
|
|
12
|
+
include Legion::Logging::Helper
|
|
13
|
+
|
|
14
|
+
# ── Static capability map for known OpenAI model families ──────
|
|
15
|
+
# Maps model-id prefixes to a set of capabilities and modality
|
|
16
|
+
# vectors. Used by list_models to build Model::Info structs from
|
|
17
|
+
# the raw /v1/models response.
|
|
18
|
+
CAPABILITY_MAP = {
|
|
19
|
+
'gpt-4o' => {
|
|
20
|
+
capabilities: %i[completion streaming function_calling vision structured_output],
|
|
21
|
+
modalities_input: %w[text image audio],
|
|
22
|
+
modalities_output: %w[text]
|
|
23
|
+
},
|
|
24
|
+
'gpt-4.1' => {
|
|
25
|
+
capabilities: %i[completion streaming function_calling vision structured_output],
|
|
26
|
+
modalities_input: %w[text image],
|
|
27
|
+
modalities_output: %w[text]
|
|
28
|
+
},
|
|
29
|
+
'gpt-4' => {
|
|
30
|
+
capabilities: %i[completion streaming function_calling vision],
|
|
31
|
+
modalities_input: %w[text image],
|
|
32
|
+
modalities_output: %w[text]
|
|
33
|
+
},
|
|
34
|
+
'gpt-5' => {
|
|
35
|
+
capabilities: %i[completion streaming function_calling vision structured_output reasoning],
|
|
36
|
+
modalities_input: %w[text image],
|
|
37
|
+
modalities_output: %w[text]
|
|
38
|
+
},
|
|
39
|
+
'o4' => {
|
|
40
|
+
capabilities: %i[completion streaming function_calling vision reasoning],
|
|
41
|
+
modalities_input: %w[text image],
|
|
42
|
+
modalities_output: %w[text]
|
|
43
|
+
},
|
|
44
|
+
'o3' => {
|
|
45
|
+
capabilities: %i[completion streaming function_calling vision reasoning],
|
|
46
|
+
modalities_input: %w[text image],
|
|
47
|
+
modalities_output: %w[text]
|
|
48
|
+
},
|
|
49
|
+
'o1' => {
|
|
50
|
+
capabilities: %i[completion streaming function_calling vision reasoning],
|
|
51
|
+
modalities_input: %w[text image],
|
|
52
|
+
modalities_output: %w[text]
|
|
53
|
+
},
|
|
54
|
+
'text-embedding-' => {
|
|
55
|
+
capabilities: %i[embedding],
|
|
56
|
+
modalities_input: %w[text],
|
|
57
|
+
modalities_output: %w[embeddings]
|
|
58
|
+
},
|
|
59
|
+
'omni-moderation' => {
|
|
60
|
+
capabilities: %i[moderation],
|
|
61
|
+
modalities_input: %w[text image],
|
|
62
|
+
modalities_output: %w[moderation]
|
|
63
|
+
},
|
|
64
|
+
'text-moderation' => {
|
|
65
|
+
capabilities: %i[moderation],
|
|
66
|
+
modalities_input: %w[text],
|
|
67
|
+
modalities_output: %w[moderation]
|
|
68
|
+
},
|
|
69
|
+
'gpt-image' => {
|
|
70
|
+
capabilities: %i[image_generation],
|
|
71
|
+
modalities_input: %w[text image],
|
|
72
|
+
modalities_output: %w[image]
|
|
73
|
+
},
|
|
74
|
+
'dall-e' => {
|
|
75
|
+
capabilities: %i[image_generation],
|
|
76
|
+
modalities_input: %w[text],
|
|
77
|
+
modalities_output: %w[image]
|
|
78
|
+
},
|
|
79
|
+
'whisper' => {
|
|
80
|
+
capabilities: %i[audio_transcription],
|
|
81
|
+
modalities_input: %w[audio],
|
|
82
|
+
modalities_output: %w[text]
|
|
83
|
+
},
|
|
84
|
+
'tts' => {
|
|
85
|
+
capabilities: %i[audio_generation],
|
|
86
|
+
modalities_input: %w[text],
|
|
87
|
+
modalities_output: %w[audio]
|
|
88
|
+
}
|
|
89
|
+
}.freeze
|
|
12
90
|
|
|
13
91
|
class << self
|
|
14
92
|
attr_writer :registry_publisher
|
|
@@ -29,7 +107,7 @@ module Legion
|
|
|
29
107
|
def capabilities = Capabilities
|
|
30
108
|
|
|
31
109
|
def registry_publisher
|
|
32
|
-
@registry_publisher ||= RegistryPublisher.new
|
|
110
|
+
@registry_publisher ||= Legion::Extensions::Llm::RegistryPublisher.new(provider_family: :openai)
|
|
33
111
|
end
|
|
34
112
|
end
|
|
35
113
|
|
|
@@ -94,17 +172,66 @@ module Legion
|
|
|
94
172
|
def images_url(with: nil, mask: nil) = super
|
|
95
173
|
|
|
96
174
|
def retrieve_model(model)
|
|
175
|
+
log.info("Retrieving model: #{model}")
|
|
97
176
|
connection.get("#{models_url}/#{model}").body
|
|
177
|
+
rescue StandardError => e
|
|
178
|
+
handle_exception(e, level: :error, handled: true,
|
|
179
|
+
operation: 'retrieve_model')
|
|
180
|
+
raise
|
|
98
181
|
end
|
|
99
182
|
|
|
100
183
|
def list_models
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
184
|
+
log.info('Listing OpenAI models')
|
|
185
|
+
raw = connection.get(models_url)
|
|
186
|
+
models = build_model_infos(raw.body)
|
|
187
|
+
log.info("Discovered #{models.size} OpenAI models")
|
|
188
|
+
self.class.registry_publisher.publish_models_async(models, readiness: readiness(live: false))
|
|
189
|
+
models
|
|
190
|
+
rescue StandardError => e
|
|
191
|
+
handle_exception(e, level: :error, handled: true,
|
|
192
|
+
operation: 'list_models')
|
|
193
|
+
raise
|
|
104
194
|
end
|
|
105
195
|
|
|
106
196
|
private
|
|
107
197
|
|
|
198
|
+
def build_model_infos(body)
|
|
199
|
+
body.fetch('data', []).map do |raw_model|
|
|
200
|
+
id = raw_model.fetch('id')
|
|
201
|
+
cap_entry = capability_entry_for(id)
|
|
202
|
+
|
|
203
|
+
Legion::Extensions::Llm::Model::Info.new(
|
|
204
|
+
id: id,
|
|
205
|
+
name: id,
|
|
206
|
+
provider: :openai,
|
|
207
|
+
capabilities: cap_entry[:capabilities],
|
|
208
|
+
modalities_input: cap_entry[:modalities_input],
|
|
209
|
+
modalities_output: cap_entry[:modalities_output],
|
|
210
|
+
metadata: {
|
|
211
|
+
created_at: model_created_at(raw_model['created']),
|
|
212
|
+
raw: raw_model
|
|
213
|
+
}.compact
|
|
214
|
+
)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def capability_entry_for(model_id)
|
|
219
|
+
CAPABILITY_MAP.each do |prefix, entry|
|
|
220
|
+
return entry if model_id.start_with?(prefix)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Fallback for unknown models: assume chat-capable
|
|
224
|
+
{
|
|
225
|
+
capabilities: %i[completion streaming],
|
|
226
|
+
modalities_input: %w[text],
|
|
227
|
+
modalities_output: %w[text]
|
|
228
|
+
}
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def model_created_at(value)
|
|
232
|
+
value.is_a?(Numeric) ? Time.at(value).utc : value
|
|
233
|
+
end
|
|
234
|
+
|
|
108
235
|
def maybe_normalize_temperature(temperature, model)
|
|
109
236
|
model_id = model.id.to_s
|
|
110
237
|
return nil if model_id.include?('-search')
|
|
@@ -117,3 +244,8 @@ module Legion
|
|
|
117
244
|
end
|
|
118
245
|
end
|
|
119
246
|
end
|
|
247
|
+
|
|
248
|
+
# Register configuration options so Legion::Extensions::Llm::Configuration knows about them.
|
|
249
|
+
Legion::Extensions::Llm::Configuration.register_provider_options(
|
|
250
|
+
Legion::Extensions::Llm::Openai::Provider.configuration_options
|
|
251
|
+
)
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'legion/extensions/llm'
|
|
4
|
-
require 'legion/extensions/llm/openai/registry_event_builder'
|
|
5
|
-
require 'legion/extensions/llm/openai/registry_publisher'
|
|
6
4
|
require 'legion/extensions/llm/openai/provider'
|
|
7
5
|
require 'legion/extensions/llm/openai/version'
|
|
8
6
|
|
|
@@ -12,21 +10,23 @@ module Legion
|
|
|
12
10
|
# Openai provider extension namespace.
|
|
13
11
|
module Openai
|
|
14
12
|
extend ::Legion::Extensions::Core if ::Legion::Extensions.const_defined?(:Core, false)
|
|
13
|
+
extend ::Legion::Logging::Helper
|
|
15
14
|
|
|
16
15
|
PROVIDER_FAMILY = :openai
|
|
17
16
|
|
|
18
17
|
def self.default_settings
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
18
|
+
{
|
|
19
|
+
enabled: false,
|
|
20
|
+
default_model: 'gpt-4o',
|
|
21
|
+
api_key: nil,
|
|
22
|
+
organization_id: nil,
|
|
23
|
+
project_id: nil,
|
|
24
|
+
model_whitelist: [],
|
|
25
|
+
model_blacklist: [],
|
|
26
|
+
model_cache_ttl: 3600,
|
|
27
|
+
tls: { enabled: false, verify: :peer },
|
|
28
|
+
instances: {}
|
|
29
|
+
}
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def self.provider_class
|
|
@@ -36,6 +36,3 @@ module Legion
|
|
|
36
36
|
end
|
|
37
37
|
end
|
|
38
38
|
end
|
|
39
|
-
|
|
40
|
-
Legion::Extensions::Llm::Provider.register(Legion::Extensions::Llm::Openai::PROVIDER_FAMILY,
|
|
41
|
-
Legion::Extensions::Llm::Openai::Provider)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lex-llm-openai
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- LegionIO
|
|
@@ -57,14 +57,14 @@ dependencies:
|
|
|
57
57
|
requirements:
|
|
58
58
|
- - ">="
|
|
59
59
|
- !ruby/object:Gem::Version
|
|
60
|
-
version: 0.
|
|
60
|
+
version: 0.2.0
|
|
61
61
|
type: :runtime
|
|
62
62
|
prerelease: false
|
|
63
63
|
version_requirements: !ruby/object:Gem::Requirement
|
|
64
64
|
requirements:
|
|
65
65
|
- - ">="
|
|
66
66
|
- !ruby/object:Gem::Version
|
|
67
|
-
version: 0.
|
|
67
|
+
version: 0.2.0
|
|
68
68
|
description: OpenAI provider integration for the LegionIO LLM routing framework.
|
|
69
69
|
email:
|
|
70
70
|
- matthewdiverson@gmail.com
|
|
@@ -84,10 +84,6 @@ files:
|
|
|
84
84
|
- lex-llm-openai.gemspec
|
|
85
85
|
- lib/legion/extensions/llm/openai.rb
|
|
86
86
|
- lib/legion/extensions/llm/openai/provider.rb
|
|
87
|
-
- lib/legion/extensions/llm/openai/registry_event_builder.rb
|
|
88
|
-
- lib/legion/extensions/llm/openai/registry_publisher.rb
|
|
89
|
-
- lib/legion/extensions/llm/openai/transport/exchanges/llm_registry.rb
|
|
90
|
-
- lib/legion/extensions/llm/openai/transport/messages/registry_event.rb
|
|
91
87
|
- lib/legion/extensions/llm/openai/version.rb
|
|
92
88
|
homepage: https://github.com/LegionIO/lex-llm-openai
|
|
93
89
|
licenses:
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Legion
|
|
4
|
-
module Extensions
|
|
5
|
-
module Llm
|
|
6
|
-
module Openai
|
|
7
|
-
# Builds sanitized lex-llm registry envelopes for OpenAI provider state.
|
|
8
|
-
class RegistryEventBuilder
|
|
9
|
-
def model_available(model, readiness:)
|
|
10
|
-
registry_event_class.available(
|
|
11
|
-
model_offering(model),
|
|
12
|
-
runtime: runtime_metadata,
|
|
13
|
-
health: model_health(readiness),
|
|
14
|
-
metadata: model_metadata(model)
|
|
15
|
-
)
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
private
|
|
19
|
-
|
|
20
|
-
def model_offering(model)
|
|
21
|
-
{
|
|
22
|
-
provider_family: :openai,
|
|
23
|
-
provider_instance: provider_instance,
|
|
24
|
-
transport: :http,
|
|
25
|
-
model: model.id,
|
|
26
|
-
usage_type: usage_type_for(model),
|
|
27
|
-
capabilities: Array(model.capabilities).map(&:to_sym),
|
|
28
|
-
limits: model_limits(model),
|
|
29
|
-
metadata: { lex: :llm_openai, model_name: model.name }.compact
|
|
30
|
-
}
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def model_health(readiness)
|
|
34
|
-
ready = readiness.fetch(:ready, true) == true
|
|
35
|
-
{ ready:, status: ready ? :available : :degraded }
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def model_metadata(model)
|
|
39
|
-
{ extension: :lex_llm_openai, provider: :openai, model_type: model.type }
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def runtime_metadata
|
|
43
|
-
{ node: provider_instance }
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def model_limits(model)
|
|
47
|
-
{
|
|
48
|
-
context_window: model.context_window,
|
|
49
|
-
max_output_tokens: model.max_output_tokens
|
|
50
|
-
}.compact
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def usage_type_for(model)
|
|
54
|
-
model.type == 'embedding' ? :embedding : :inference
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def provider_instance
|
|
58
|
-
configured_node = (::Legion::Settings.dig(:node, :canonical_name) if defined?(::Legion::Settings))
|
|
59
|
-
value = configured_node.to_s.strip
|
|
60
|
-
value.empty? ? :openai : value.to_sym
|
|
61
|
-
rescue StandardError
|
|
62
|
-
:openai
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def registry_event_class
|
|
66
|
-
::Legion::Extensions::Llm::Routing::RegistryEvent
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
end
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Legion
|
|
4
|
-
module Extensions
|
|
5
|
-
module Llm
|
|
6
|
-
module Openai
|
|
7
|
-
# Best-effort publisher for OpenAI provider availability events.
|
|
8
|
-
class RegistryPublisher
|
|
9
|
-
APP_ID = 'lex-llm-openai'
|
|
10
|
-
|
|
11
|
-
def initialize(builder: RegistryEventBuilder.new)
|
|
12
|
-
@builder = builder
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def publish_models_async(models, readiness:)
|
|
16
|
-
schedule do
|
|
17
|
-
Array(models).each do |model|
|
|
18
|
-
publish_event(@builder.model_available(model, readiness:))
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
private
|
|
24
|
-
|
|
25
|
-
def schedule(&)
|
|
26
|
-
return false unless publishing_available?
|
|
27
|
-
|
|
28
|
-
Thread.new do
|
|
29
|
-
Thread.current.abort_on_exception = false
|
|
30
|
-
yield
|
|
31
|
-
rescue StandardError => e
|
|
32
|
-
log_publish_failure(e, level: :debug)
|
|
33
|
-
end
|
|
34
|
-
rescue StandardError => e
|
|
35
|
-
log_publish_failure(e, level: :debug)
|
|
36
|
-
false
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def publish_event(event)
|
|
40
|
-
return false unless publishing_available?
|
|
41
|
-
|
|
42
|
-
message_class.new(event:, app_id: APP_ID).publish(spool: false)
|
|
43
|
-
rescue StandardError => e
|
|
44
|
-
log_publish_failure(e)
|
|
45
|
-
false
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def publishing_available?
|
|
49
|
-
return false unless registry_event_available?
|
|
50
|
-
return false unless transport_message_available?
|
|
51
|
-
return true unless defined?(::Legion::Transport::Connection)
|
|
52
|
-
return true unless ::Legion::Transport::Connection.respond_to?(:session_open?)
|
|
53
|
-
|
|
54
|
-
::Legion::Transport::Connection.session_open?
|
|
55
|
-
rescue StandardError
|
|
56
|
-
false
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def registry_event_available?
|
|
60
|
-
defined?(::Legion::Extensions::Llm::Routing::RegistryEvent)
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def transport_message_available?
|
|
64
|
-
return true if message_class_defined?
|
|
65
|
-
return false unless defined?(::Legion::Transport::Message) && defined?(::Legion::Transport::Exchange)
|
|
66
|
-
|
|
67
|
-
require 'legion/extensions/llm/openai/transport/messages/registry_event'
|
|
68
|
-
message_class_defined?
|
|
69
|
-
rescue LoadError
|
|
70
|
-
false
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def message_class_defined?
|
|
74
|
-
defined?(::Legion::Extensions::Llm::Openai::Transport::Messages::RegistryEvent)
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def message_class
|
|
78
|
-
::Legion::Extensions::Llm::Openai::Transport::Messages::RegistryEvent
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def log_publish_failure(error, level: :warn)
|
|
82
|
-
message = "[lex-llm-openai] llm.registry publish failed: #{error.class}: #{error.message}"
|
|
83
|
-
logger = ::Legion::Extensions::Llm.logger if defined?(::Legion::Extensions::Llm)
|
|
84
|
-
if logger.respond_to?(level)
|
|
85
|
-
logger.public_send(level, message)
|
|
86
|
-
elsif logger.respond_to?(:debug)
|
|
87
|
-
logger.debug(message)
|
|
88
|
-
end
|
|
89
|
-
rescue StandardError
|
|
90
|
-
nil
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
end
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Legion
|
|
4
|
-
module Extensions
|
|
5
|
-
module Llm
|
|
6
|
-
module Openai
|
|
7
|
-
module Transport
|
|
8
|
-
module Exchanges
|
|
9
|
-
# Topic exchange for OpenAI provider availability events.
|
|
10
|
-
class LlmRegistry < ::Legion::Transport::Exchange
|
|
11
|
-
def exchange_name
|
|
12
|
-
'llm.registry'
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def default_type
|
|
16
|
-
'topic'
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'legion/extensions/llm/openai/transport/exchanges/llm_registry'
|
|
4
|
-
|
|
5
|
-
module Legion
|
|
6
|
-
module Extensions
|
|
7
|
-
module Llm
|
|
8
|
-
module Openai
|
|
9
|
-
module Transport
|
|
10
|
-
module Messages
|
|
11
|
-
# Publishes lex-llm RegistryEvent envelopes to the llm.registry exchange.
|
|
12
|
-
class RegistryEvent < ::Legion::Transport::Message
|
|
13
|
-
def initialize(event:, **options)
|
|
14
|
-
super(**event.to_h.merge(options))
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def exchange
|
|
18
|
-
Transport::Exchanges::LlmRegistry
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def routing_key
|
|
22
|
-
@options[:routing_key] || "llm.registry.#{@options.fetch(:event_type)}"
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def type
|
|
26
|
-
'llm.registry.event'
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def app_id
|
|
30
|
-
@options[:app_id] || RegistryPublisher::APP_ID
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def persistent # rubocop:disable Naming/PredicateMethod
|
|
34
|
-
false
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
end
|