lex-llm-openai 0.1.8 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 923d77b5cf761e20eccb817d66dd36bec5310842f1216c2b403432bf84bf11fa
4
- data.tar.gz: e84b863710db389adf98f1b06783c46e4e4b7797c51889026c62939a3e3ad9a6
3
+ metadata.gz: 3adf2416815789eee25cb13dc0ceaadda14549fa6f897ea8c0216a5ebd728793
4
+ data.tar.gz: 317ad6bcc34b252a9026e9aaae5a9a6fe8f850ab67c7e50e62f67a5c43d3538f
5
5
  SHA512:
6
- metadata.gz: 3ea2f8c543f3e8cbdf55a158bb0033d1711b34b3114ea8ebbccc530d398ac3931e95d3c729be869eac5070dac773edec62e3de1dc94b3c00a4560c6b6a998cbe
7
- data.tar.gz: 5672bc7469d4a09a12c85fcd343c8008748ccb6a90f85dc54cfdf7feb9d81721a026aa91a11d555b826159fbe61a528732301cf19ab63470800068faa1971a42
6
+ metadata.gz: 6e5b1684b8a9a328278bb0ab039940f924f43943847fe45d0b485c82702f4cbb426b142e751d407ed736778b9923bfffd8e8b323549bef335c99056f989c4250
7
+ data.tar.gz: 0ff1be0c6ccd2fa4e24a3c9bb8dc816e6dc249ff6a88177240be0f5fc4aedade94cb9955d752f28570af099a64e2dea232e3d7bdef0587ab7ef8a308dbe23684
data/.rubocop.yml CHANGED
@@ -12,7 +12,11 @@ Metrics/BlockLength:
12
12
  Exclude:
13
13
  - "*.gemspec"
14
14
  - spec/**/*
15
+ Metrics/ClassLength:
16
+ Max: 200
15
17
  Metrics/MethodLength:
16
18
  Enabled: false
19
+ RSpec/ExampleLength:
20
+ Max: 10
17
21
  RSpec/MultipleExpectations:
18
22
  Enabled: false
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
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
+
3
12
  ## [0.1.8] - 2026-04-30
4
13
  - Add Legion::Logging::Helper to all modules and classes for structured observability
5
14
  - Replace bare rescue blocks with handle_exception for unified error telemetry
@@ -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.1.5'
29
+ spec.add_dependency 'lex-llm', '>= 0.2.0'
30
30
  end
@@ -9,7 +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 if defined?(Legion::Logging::Helper)
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
13
90
 
14
91
  class << self
15
92
  attr_writer :registry_publisher
@@ -30,7 +107,7 @@ module Legion
30
107
  def capabilities = Capabilities
31
108
 
32
109
  def registry_publisher
33
- @registry_publisher ||= RegistryPublisher.new
110
+ @registry_publisher ||= Legion::Extensions::Llm::RegistryPublisher.new(provider_family: :openai)
34
111
  end
35
112
  end
36
113
 
@@ -95,32 +172,66 @@ module Legion
95
172
  def images_url(with: nil, mask: nil) = super
96
173
 
97
174
  def retrieve_model(model)
98
- log.info("Retrieving model: #{model}") if respond_to?(:log)
175
+ log.info("Retrieving model: #{model}")
99
176
  connection.get("#{models_url}/#{model}").body
100
177
  rescue StandardError => e
101
- if respond_to?(:handle_exception)
102
- handle_exception(e, level: :error, handled: true,
103
- operation: 'retrieve_model')
104
- end
178
+ handle_exception(e, level: :error, handled: true,
179
+ operation: 'retrieve_model')
105
180
  raise
106
181
  end
107
182
 
108
183
  def list_models
109
- log.info('Listing OpenAI models') if respond_to?(:log)
110
- super.tap do |models|
111
- log.info("Discovered #{models.size} OpenAI models") if respond_to?(:log)
112
- self.class.registry_publisher.publish_models_async(models, readiness: readiness(live: false))
113
- end
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
114
190
  rescue StandardError => e
115
- if respond_to?(:handle_exception)
116
- handle_exception(e, level: :error, handled: true,
117
- operation: 'list_models')
118
- end
191
+ handle_exception(e, level: :error, handled: true,
192
+ operation: 'list_models')
119
193
  raise
120
194
  end
121
195
 
122
196
  private
123
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
+
124
235
  def maybe_normalize_temperature(temperature, model)
125
236
  model_id = model.id.to_s
126
237
  return nil if model_id.include?('-search')
@@ -133,3 +244,8 @@ module Legion
133
244
  end
134
245
  end
135
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
+ )
@@ -4,7 +4,7 @@ module Legion
4
4
  module Extensions
5
5
  module Llm
6
6
  module Openai
7
- VERSION = '0.1.8'
7
+ VERSION = '0.2.0'
8
8
  end
9
9
  end
10
10
  end
@@ -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,22 +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)
15
- extend ::Legion::Logging::Helper if defined?(::Legion::Logging::Helper)
13
+ extend ::Legion::Logging::Helper
16
14
 
17
15
  PROVIDER_FAMILY = :openai
18
16
 
19
17
  def self.default_settings
20
- ::Legion::Extensions::Llm.provider_settings(
21
- family: PROVIDER_FAMILY,
22
- instance: {
23
- endpoint: 'https://api.openai.com',
24
- tier: :frontier,
25
- transport: :http,
26
- credentials: { api_key: 'env://OPENAI_API_KEY' },
27
- usage: { inference: true, embedding: true, moderation: true, image: true, audio: true },
28
- limits: { concurrency: 4 }
29
- }
30
- )
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
+ }
31
30
  end
32
31
 
33
32
  def self.provider_class
@@ -37,9 +36,3 @@ module Legion
37
36
  end
38
37
  end
39
38
  end
40
-
41
- Legion::Extensions::Llm::Provider.register(Legion::Extensions::Llm::Openai::PROVIDER_FAMILY,
42
- Legion::Extensions::Llm::Openai::Provider)
43
- if defined?(Legion::Logging::Helper) && Legion::Extensions::Llm::Openai.respond_to?(:log)
44
- Legion::Extensions::Llm::Openai.log.info('Registered OpenAI provider for :openai family')
45
- end
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.1.8
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.1.5
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.1.5
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,78 +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
- include Legion::Logging::Helper if defined?(Legion::Logging::Helper)
10
-
11
- def model_available(model, readiness:)
12
- registry_event_class.available(
13
- model_offering(model),
14
- runtime: runtime_metadata,
15
- health: model_health(readiness),
16
- metadata: model_metadata(model)
17
- )
18
- end
19
-
20
- private
21
-
22
- def model_offering(model)
23
- {
24
- provider_family: :openai,
25
- provider_instance: provider_instance,
26
- transport: :http,
27
- model: model.id,
28
- usage_type: usage_type_for(model),
29
- capabilities: Array(model.capabilities).map(&:to_sym),
30
- limits: model_limits(model),
31
- metadata: { lex: :llm_openai, model_name: model.name }.compact
32
- }
33
- end
34
-
35
- def model_health(readiness)
36
- ready = readiness.fetch(:ready, true) == true
37
- { ready:, status: ready ? :available : :degraded }
38
- end
39
-
40
- def model_metadata(model)
41
- { extension: :lex_llm_openai, provider: :openai, model_type: model.type }
42
- end
43
-
44
- def runtime_metadata
45
- { node: provider_instance }
46
- end
47
-
48
- def model_limits(model)
49
- {
50
- context_window: model.context_window,
51
- max_output_tokens: model.max_output_tokens
52
- }.compact
53
- end
54
-
55
- def usage_type_for(model)
56
- model.type == 'embedding' ? :embedding : :inference
57
- end
58
-
59
- def provider_instance
60
- configured_node = (::Legion::Settings.dig(:node, :canonical_name) if defined?(::Legion::Settings))
61
- value = configured_node.to_s.strip
62
- value.empty? ? :openai : value.to_sym
63
- rescue StandardError => e
64
- if respond_to?(:handle_exception)
65
- handle_exception(e, level: :debug, handled: true,
66
- operation: 'provider_instance')
67
- end
68
- :openai
69
- end
70
-
71
- def registry_event_class
72
- ::Legion::Extensions::Llm::Routing::RegistryEvent
73
- end
74
- end
75
- end
76
- end
77
- end
78
- end
@@ -1,98 +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
- include Legion::Logging::Helper if defined?(Legion::Logging::Helper)
10
-
11
- APP_ID = 'lex-llm-openai'
12
-
13
- def initialize(builder: RegistryEventBuilder.new)
14
- @builder = builder
15
- end
16
-
17
- def publish_models_async(models, readiness:)
18
- log.info("Publishing #{Array(models).size} model(s) to llm.registry") if respond_to?(:log)
19
- schedule do
20
- Array(models).each do |model|
21
- publish_event(@builder.model_available(model, readiness:))
22
- end
23
- end
24
- end
25
-
26
- private
27
-
28
- def schedule(&)
29
- return false unless publishing_available?
30
-
31
- Thread.new do
32
- Thread.current.abort_on_exception = false
33
- yield
34
- rescue StandardError => e
35
- handle_exception(e, level: :debug, handled: true, operation: 'schedule') if respond_to?(:handle_exception)
36
- end
37
- rescue StandardError => e
38
- handle_exception(e, level: :debug, handled: true, operation: 'schedule') if respond_to?(:handle_exception)
39
- false
40
- end
41
-
42
- def publish_event(event)
43
- return false unless publishing_available?
44
-
45
- message_class.new(event:, app_id: APP_ID).publish(spool: false)
46
- rescue StandardError => e
47
- if respond_to?(:handle_exception)
48
- handle_exception(e, level: :warn, handled: true,
49
- operation: 'publish_event')
50
- end
51
- false
52
- end
53
-
54
- def publishing_available?
55
- return false unless registry_event_available?
56
- return false unless transport_message_available?
57
- return true unless defined?(::Legion::Transport::Connection)
58
- return true unless ::Legion::Transport::Connection.respond_to?(:session_open?)
59
-
60
- ::Legion::Transport::Connection.session_open?
61
- rescue StandardError => e
62
- if respond_to?(:handle_exception)
63
- handle_exception(e, level: :debug, handled: true,
64
- operation: 'publishing_available?')
65
- end
66
- false
67
- end
68
-
69
- def registry_event_available?
70
- defined?(::Legion::Extensions::Llm::Routing::RegistryEvent)
71
- end
72
-
73
- def transport_message_available?
74
- return true if message_class_defined?
75
- return false unless defined?(::Legion::Transport::Message) && defined?(::Legion::Transport::Exchange)
76
-
77
- require 'legion/extensions/llm/openai/transport/messages/registry_event'
78
- message_class_defined?
79
- rescue LoadError => e
80
- if respond_to?(:handle_exception)
81
- handle_exception(e, level: :debug, handled: true,
82
- operation: 'transport_message_available?')
83
- end
84
- false
85
- end
86
-
87
- def message_class_defined?
88
- defined?(::Legion::Extensions::Llm::Openai::Transport::Messages::RegistryEvent)
89
- end
90
-
91
- def message_class
92
- ::Legion::Extensions::Llm::Openai::Transport::Messages::RegistryEvent
93
- end
94
- end
95
- end
96
- end
97
- end
98
- 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