lex-llm-openai 0.4.5 → 0.4.8
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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9795e7797d7d5f8ce0994e16625e2ccd6d2ef54299166e9bbec86bd2ce7c002c
|
|
4
|
+
data.tar.gz: 4c5eb2fa507ae61c44893be81a7ef2a7b692542fb3bef191ece53e0d061b68e2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f60720c95320eed7e1a2d02e3fd91857ad290c2ad06c525bf12321a309b09bb7529232bf0461a6ed7dbd38a81c6ac912f7025c6d3de35d3872e90ccc5675b326
|
|
7
|
+
data.tar.gz: 435f627775ea601c6d3c41f850d6bf9efcabea039692ac2c06161b990b0efde1b59d7c95ba837e6f2bfffd4ced82a4afe2fef1439fd5200c203e7b4dec7e416a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.8] - 2026-06-20
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- Stop bulk-publishing OpenAI model availability from `list_models`; discovery now emits one registry event per seen model from the shared `lex-llm` policy-filter path so blocked models stay observable without duplicate publishes.
|
|
7
|
+
|
|
8
|
+
## [0.4.7] - 2026-06-20
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Normalize OpenAI offering capabilities through the canonical `lex-llm` contract so `completion`, `embedding`, `thinking`, image, and audio capabilities survive discovery without provider-specific vocabulary drift.
|
|
12
|
+
- Move provider/instance/model capability override extraction onto the shared base provider implementation.
|
|
13
|
+
|
|
14
|
+
## [0.4.6] - 2026-06-19
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- Adopt `Legion::Extensions::Llm::Inventory::ScopedRefresher` mixin (lex-llm 0.6.0). Discovery
|
|
18
|
+
refresh actors now write directly to the live `Inventory` catalog via `Inventory.write_lane`.
|
|
19
|
+
- Pin `lex-llm >= 0.6.0` and `legion-llm >= 0.14.0` in gemspec.
|
|
20
|
+
- Standard `weight: 100` default added to provider instance settings schema.
|
|
21
|
+
|
|
3
22
|
## 0.4.5 - 2026-06-17
|
|
4
23
|
|
|
5
24
|
### Changed
|
data/lex-llm-openai.gemspec
CHANGED
|
@@ -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,5 +1,7 @@
|
|
|
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
|
|
@@ -8,6 +10,12 @@ end
|
|
|
8
10
|
|
|
9
11
|
return unless defined?(Legion::Extensions::Actors::Every)
|
|
10
12
|
|
|
13
|
+
begin
|
|
14
|
+
require 'legion/extensions/llm/inventory/scoped_refresher'
|
|
15
|
+
rescue LoadError => e
|
|
16
|
+
warn(e.message) if $VERBOSE
|
|
17
|
+
end
|
|
18
|
+
|
|
11
19
|
module Legion
|
|
12
20
|
module Extensions
|
|
13
21
|
module Llm
|
|
@@ -17,7 +25,11 @@ module Legion
|
|
|
17
25
|
class DiscoveryRefresh < Legion::Extensions::Actors::Every
|
|
18
26
|
include Legion::Logging::Helper
|
|
19
27
|
|
|
20
|
-
|
|
28
|
+
if defined?(Legion::Extensions::Llm::Inventory::ScopedRefresher)
|
|
29
|
+
include Legion::Extensions::Llm::Inventory::ScopedRefresher
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.every_seconds = 3600
|
|
21
33
|
|
|
22
34
|
def runner_class = self.class
|
|
23
35
|
def runner_function = 'manual'
|
|
@@ -27,26 +39,135 @@ module Legion
|
|
|
27
39
|
def generate_task? = false
|
|
28
40
|
|
|
29
41
|
def time
|
|
30
|
-
return
|
|
42
|
+
return self.class.every_seconds unless defined?(Legion::Settings)
|
|
31
43
|
|
|
32
|
-
Legion::Settings.dig(:extensions, :llm, :openai, :discovery_interval) ||
|
|
44
|
+
Legion::Settings.dig(:extensions, :llm, :openai, :discovery_interval) || self.class.every_seconds
|
|
33
45
|
end
|
|
34
46
|
|
|
35
|
-
def
|
|
36
|
-
|
|
37
|
-
|
|
47
|
+
def scope_key
|
|
48
|
+
{ provider: :openai }
|
|
49
|
+
end
|
|
38
50
|
|
|
39
|
-
|
|
51
|
+
def compute_lanes_for_scope
|
|
52
|
+
return [] unless defined?(Legion::LLM::Call::Registry)
|
|
40
53
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
end
|
|
44
|
-
if defined?(Legion::LLM::Inventory) && Legion::LLM::Inventory.respond_to?(:invalidate_offerings_cache!)
|
|
45
|
-
Legion::LLM::Inventory.invalidate_offerings_cache!
|
|
54
|
+
instances = Legion::LLM::Call::Registry.all_instances.select do |e|
|
|
55
|
+
(e[:provider] || '').to_sym == :openai
|
|
46
56
|
end
|
|
57
|
+
|
|
58
|
+
lanes = []
|
|
59
|
+
instances.each { |entry| lanes.concat(lanes_for_instance(entry)) }
|
|
60
|
+
lanes
|
|
61
|
+
rescue StandardError => e
|
|
62
|
+
handle_exception(e, level: :warn, handled: true,
|
|
63
|
+
operation: 'openai.actor.discovery_refresh.compute_lanes')
|
|
64
|
+
[]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def credential_hash
|
|
68
|
+
settings = Legion::Settings.dig(:extensions, :llm, :openai) || {}
|
|
69
|
+
Digest::SHA256.hexdigest(settings[:api_key].to_s + settings[:instances].to_s)[0, 16]
|
|
70
|
+
rescue StandardError
|
|
71
|
+
'unknown'
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def manual
|
|
75
|
+
tick_if_scoped_refresher
|
|
47
76
|
rescue StandardError => e
|
|
48
77
|
handle_exception(e, level: :warn, handled: true, operation: 'openai.actor.discovery_refresh')
|
|
49
78
|
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def tick_if_scoped_refresher
|
|
83
|
+
return unless defined?(Legion::Extensions::Llm::Inventory::ScopedRefresher)
|
|
84
|
+
return unless self.class.ancestors.include?(Legion::Extensions::Llm::Inventory::ScopedRefresher)
|
|
85
|
+
|
|
86
|
+
tick
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def lanes_for_instance(instance_entry) # rubocop:disable Metrics/CyclomaticComplexity
|
|
90
|
+
adapter = instance_entry[:adapter]
|
|
91
|
+
return [] unless adapter.respond_to?(:discover_offerings)
|
|
92
|
+
|
|
93
|
+
instance_id = instance_entry[:instance] || instance_entry[:instance_id] ||
|
|
94
|
+
instance_entry[:id] || :default
|
|
95
|
+
lanes = []
|
|
96
|
+
|
|
97
|
+
Array(adapter.discover_offerings(live: true)).each do |raw_offering|
|
|
98
|
+
offering = offering_to_hash(raw_offering)
|
|
99
|
+
next unless offering
|
|
100
|
+
|
|
101
|
+
lane = build_lane(offering, instance_id)
|
|
102
|
+
lanes << lane
|
|
103
|
+
fleet_lane = maybe_fleet_lane(offering, lane)
|
|
104
|
+
lanes << fleet_lane if fleet_lane
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
lanes
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def offering_to_hash(offering)
|
|
111
|
+
return nil if offering.nil?
|
|
112
|
+
return offering if offering.is_a?(Hash)
|
|
113
|
+
|
|
114
|
+
hash = offering.to_h
|
|
115
|
+
hash[:type] ||= hash[:usage_type]
|
|
116
|
+
hash[:enabled] = offering.respond_to?(:enabled?) ? offering.enabled? : true
|
|
117
|
+
hash
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def build_lane(offering, instance_id)
|
|
121
|
+
tier = offering[:tier] || :frontier
|
|
122
|
+
type = offering_type(offering)
|
|
123
|
+
lane_fields = { tier: tier, provider_family: :openai, instance_id: instance_id,
|
|
124
|
+
type: type, model: offering[:model] }
|
|
125
|
+
{
|
|
126
|
+
id: Legion::Extensions::Llm::Inventory::ScopedRefresher.compose_id(lane_fields),
|
|
127
|
+
tier: tier,
|
|
128
|
+
provider_family: :openai,
|
|
129
|
+
instance_id: instance_id,
|
|
130
|
+
model: offering[:model],
|
|
131
|
+
canonical_model_alias: offering[:canonical_model_alias],
|
|
132
|
+
type: type,
|
|
133
|
+
capabilities: normalize_capabilities(offering[:capabilities]),
|
|
134
|
+
limits: offering[:limits] || {},
|
|
135
|
+
enabled: offering.fetch(:enabled, true),
|
|
136
|
+
cost: offering[:cost]
|
|
137
|
+
}
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def maybe_fleet_lane(offering, lane)
|
|
141
|
+
return nil unless offering_type(offering) == :inference
|
|
142
|
+
|
|
143
|
+
settings = Legion::Settings.dig(:extensions, :llm, :openai) || {}
|
|
144
|
+
return nil unless settings[:fleet]&.dig(:dispatch, :enabled)
|
|
145
|
+
|
|
146
|
+
fleet_fields = {
|
|
147
|
+
tier: :fleet,
|
|
148
|
+
provider_family: lane[:provider_family],
|
|
149
|
+
instance_id: lane[:instance_id],
|
|
150
|
+
type: lane[:type],
|
|
151
|
+
model: lane[:model]
|
|
152
|
+
}
|
|
153
|
+
lane.merge(
|
|
154
|
+
id: Legion::Extensions::Llm::Inventory::ScopedRefresher.compose_id(fleet_fields),
|
|
155
|
+
tier: :fleet
|
|
156
|
+
)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def offering_type(offering)
|
|
160
|
+
%i[embed embedding].include?(offering[:type]) ? :embedding : :inference
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def normalize_capabilities(caps)
|
|
164
|
+
if defined?(Legion::Extensions::Llm::Inventory::Capabilities) &&
|
|
165
|
+
Legion::Extensions::Llm::Inventory::Capabilities.respond_to?(:normalize)
|
|
166
|
+
Legion::Extensions::Llm::Inventory::Capabilities.normalize(caps)
|
|
167
|
+
else
|
|
168
|
+
Array(caps)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
50
171
|
end
|
|
51
172
|
end
|
|
52
173
|
end
|
|
@@ -202,8 +202,7 @@ module Legion
|
|
|
202
202
|
log.debug('Listing OpenAI models')
|
|
203
203
|
raw = connection.get(models_url)
|
|
204
204
|
models = build_model_infos(raw.body)
|
|
205
|
-
log.debug { "Discovered #{models.size} OpenAI models
|
|
206
|
-
self.class.registry_publisher.publish_models_async(models, readiness: readiness(live: false))
|
|
205
|
+
log.debug { "Discovered #{models.size} OpenAI models" }
|
|
207
206
|
models
|
|
208
207
|
rescue StandardError => e
|
|
209
208
|
handle_exception(e, level: :error, handled: true,
|
|
@@ -211,17 +210,17 @@ module Legion
|
|
|
211
210
|
raise
|
|
212
211
|
end
|
|
213
212
|
|
|
214
|
-
def discover_offerings(live: false, **)
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
213
|
+
def discover_offerings(live: false, raise_on_unreachable: false, **filters)
|
|
214
|
+
return filter_cached_offerings(Array(@cached_offerings), filters) unless live
|
|
215
|
+
|
|
216
|
+
provider_health = health(live:)
|
|
217
|
+
@cached_offerings = discover_live_offerings(filters, provider_health, live:)
|
|
218
|
+
log_discover_complete(@cached_offerings)
|
|
219
|
+
@cached_offerings
|
|
220
|
+
rescue Faraday::ConnectionFailed, Faraday::TimeoutError => e
|
|
221
|
+
log.warn("[#{slug}] instance=#{provider_instance_id} unreachable: #{e.message}")
|
|
222
|
+
raise if raise_on_unreachable
|
|
223
|
+
|
|
225
224
|
[]
|
|
226
225
|
end
|
|
227
226
|
|
|
@@ -229,20 +228,57 @@ module Legion
|
|
|
229
228
|
# Maps raw CAPABILITY_MAP symbol arrays to the boolean hash format
|
|
230
229
|
# that CapabilityPolicy.resolve expects as :provider_catalog.
|
|
231
230
|
CATALOG_CAPABILITY_MAPPING = {
|
|
231
|
+
completion: :completion,
|
|
232
232
|
streaming: :streaming,
|
|
233
233
|
function_calling: :tools,
|
|
234
234
|
tools: :tools,
|
|
235
235
|
vision: :vision,
|
|
236
236
|
structured_output: :structured_output,
|
|
237
237
|
reasoning: :thinking,
|
|
238
|
-
embedding: :
|
|
238
|
+
embedding: :embedding,
|
|
239
239
|
image_generation: :image,
|
|
240
240
|
audio_transcription: :audio_transcription,
|
|
241
241
|
audio_generation: :audio_speech
|
|
242
242
|
}.freeze
|
|
243
243
|
|
|
244
|
+
def discover_live_offerings(filters, provider_health, live:)
|
|
245
|
+
readiness = discovery_registry_readiness(provider_health, live:)
|
|
246
|
+
Array(list_models(live:, **filters)).filter_map do |model|
|
|
247
|
+
self.class.registry_publisher.publish_models_async([model], readiness:)
|
|
248
|
+
next unless model_matches_filters?(model, filters)
|
|
249
|
+
next unless model_allowed?(model.id)
|
|
250
|
+
|
|
251
|
+
log_model_discovered(model)
|
|
252
|
+
offering_from_model(model, health: provider_health)
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def log_model_discovered(model)
|
|
257
|
+
log.debug(
|
|
258
|
+
"[#{slug}] instance=#{provider_instance_id} action=model_discovered " \
|
|
259
|
+
"model=#{model.id} family=#{model.family}"
|
|
260
|
+
)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def log_discover_complete(offerings)
|
|
264
|
+
log.info(
|
|
265
|
+
"[#{slug}] instance=#{provider_instance_id} action=discover_complete " \
|
|
266
|
+
"model_count=#{Array(offerings).size}"
|
|
267
|
+
)
|
|
268
|
+
end
|
|
269
|
+
|
|
244
270
|
private
|
|
245
271
|
|
|
272
|
+
def discovery_registry_readiness(provider_health, live:)
|
|
273
|
+
{
|
|
274
|
+
provider: slug.to_sym,
|
|
275
|
+
configured: configured?,
|
|
276
|
+
ready: provider_health[:ready] == true,
|
|
277
|
+
live: live,
|
|
278
|
+
health: provider_health
|
|
279
|
+
}
|
|
280
|
+
end
|
|
281
|
+
|
|
246
282
|
def build_model_infos(body)
|
|
247
283
|
body.fetch('data', []).map do |raw_model|
|
|
248
284
|
id = raw_model.fetch('id')
|
|
@@ -278,11 +314,11 @@ module Legion
|
|
|
278
314
|
}
|
|
279
315
|
end
|
|
280
316
|
|
|
281
|
-
def offering_from_model(model_info)
|
|
317
|
+
def offering_from_model(model_info, health: {})
|
|
282
318
|
policy = resolve_model_policy(model_info)
|
|
283
319
|
|
|
284
320
|
Legion::Extensions::Llm::Routing::ModelOffering.new(
|
|
285
|
-
offering_attrs_for(model_info, policy)
|
|
321
|
+
offering_attrs_for(model_info, policy, health:)
|
|
286
322
|
)
|
|
287
323
|
end
|
|
288
324
|
|
|
@@ -300,23 +336,32 @@ module Legion
|
|
|
300
336
|
)
|
|
301
337
|
end
|
|
302
338
|
|
|
303
|
-
def offering_attrs_for(model_info, policy)
|
|
339
|
+
def offering_attrs_for(model_info, policy, health: {})
|
|
304
340
|
{
|
|
305
341
|
provider_family: :openai,
|
|
306
|
-
instance_id:
|
|
307
|
-
transport:
|
|
308
|
-
tier:
|
|
342
|
+
instance_id: offering_instance_id,
|
|
343
|
+
transport: offering_transport,
|
|
344
|
+
tier: offering_tier,
|
|
309
345
|
model: model_info.id,
|
|
310
|
-
canonical_model_alias: model_info
|
|
346
|
+
canonical_model_alias: offering_alias(model_info),
|
|
311
347
|
model_family: infer_model_family(model_info.id),
|
|
312
348
|
usage_type: infer_usage_type(model_info),
|
|
313
349
|
capabilities: policy[:capabilities],
|
|
314
350
|
capability_sources: policy[:sources],
|
|
315
351
|
limits: { context_window: model_info.context_length }.compact,
|
|
352
|
+
health: health,
|
|
316
353
|
metadata: { capability_sources: policy[:sources] }
|
|
317
354
|
}
|
|
318
355
|
end
|
|
319
356
|
|
|
357
|
+
def offering_instance_id
|
|
358
|
+
config.respond_to?(:instance_id) ? config.instance_id : :default
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def offering_alias(model_info)
|
|
362
|
+
model_info.respond_to?(:name) ? model_info.name : nil
|
|
363
|
+
end
|
|
364
|
+
|
|
320
365
|
def capabilities_to_boolean_hash(capability_symbols)
|
|
321
366
|
return {} unless capability_symbols.is_a?(Array)
|
|
322
367
|
|
|
@@ -328,49 +373,6 @@ module Legion
|
|
|
328
373
|
result
|
|
329
374
|
end
|
|
330
375
|
|
|
331
|
-
def provider_capability_config
|
|
332
|
-
return {} unless defined?(Legion::Extensions::Llm::CredentialSources)
|
|
333
|
-
|
|
334
|
-
conf = Legion::Extensions::Llm::CredentialSources.setting(:extensions, :llm, :openai)
|
|
335
|
-
conf.is_a?(Hash) ? conf.to_h.except(:instances, 'instances') : {}
|
|
336
|
-
rescue StandardError => e
|
|
337
|
-
handle_exception(e, level: :debug, handled: true, operation: 'openai.provider_capability_config')
|
|
338
|
-
{}
|
|
339
|
-
end
|
|
340
|
-
|
|
341
|
-
def instance_capability_config
|
|
342
|
-
cfg = config
|
|
343
|
-
result = {}
|
|
344
|
-
%i[capabilities enable_thinking enable_tools enable_streaming enable_vision enable_embeddings
|
|
345
|
-
thinking_flag tools_flag streaming_flag vision_flag embedding_flag embeddings_flag
|
|
346
|
-
tool_flag images_flag image_flag].each do |key|
|
|
347
|
-
next unless cfg.respond_to?(key)
|
|
348
|
-
|
|
349
|
-
val = cfg.send(key)
|
|
350
|
-
result[key] = val unless val.nil?
|
|
351
|
-
rescue StandardError
|
|
352
|
-
next
|
|
353
|
-
end
|
|
354
|
-
result
|
|
355
|
-
end
|
|
356
|
-
|
|
357
|
-
def model_capability_config(model_id)
|
|
358
|
-
models_conf = fetch_models_config
|
|
359
|
-
return {} unless models_conf.respond_to?(:to_h)
|
|
360
|
-
|
|
361
|
-
models_conf.to_h[model_id.to_s] || models_conf.to_h[model_id.to_sym] || {}
|
|
362
|
-
rescue StandardError => e
|
|
363
|
-
handle_exception(e, level: :debug, handled: true, operation: 'openai.model_capability_config')
|
|
364
|
-
{}
|
|
365
|
-
end
|
|
366
|
-
|
|
367
|
-
def fetch_models_config
|
|
368
|
-
return config.models if config.respond_to?(:models)
|
|
369
|
-
return config[:models] if config.respond_to?(:[])
|
|
370
|
-
|
|
371
|
-
nil
|
|
372
|
-
end
|
|
373
|
-
|
|
374
376
|
def infer_model_family(model_id)
|
|
375
377
|
CAPABILITY_MAP.each_key do |prefix|
|
|
376
378
|
return prefix.tr('-', '_').to_sym if model_id.start_with?(prefix)
|
|
@@ -382,7 +384,7 @@ module Legion
|
|
|
382
384
|
caps = model_info.respond_to?(:capabilities) ? Array(model_info.capabilities) : []
|
|
383
385
|
return :embedding if caps.include?(:embedding)
|
|
384
386
|
return :moderation if caps.include?(:moderation)
|
|
385
|
-
return :image if caps.include?(:
|
|
387
|
+
return :image if caps.include?(:image)
|
|
386
388
|
|
|
387
389
|
:inference
|
|
388
390
|
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.4.
|
|
4
|
+
version: 0.4.8
|
|
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: OpenAI provider integration for the LegionIO LLM routing framework.
|
|
83
83
|
email:
|
|
84
84
|
- matthewdiverson@gmail.com
|