lex-llm-openai 0.4.1 → 0.4.4
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 +15 -0
- data/Gemfile +0 -6
- data/lib/legion/extensions/llm/openai/actors/discovery_refresh.rb +7 -0
- data/lib/legion/extensions/llm/openai/provider.rb +146 -7
- data/lib/legion/extensions/llm/openai/translator.rb +20 -2
- data/lib/legion/extensions/llm/openai/version.rb +1 -1
- data/lib/legion/extensions/llm/openai.rb +3 -6
- 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: 7bb7451045c6e85e6e89657665065fe502888f49982d3dc43a53fdcb1bf4ebcd
|
|
4
|
+
data.tar.gz: 05e91bb97ec042142b126b310e9e05dcfb99fe1ae999b8595f7924102106d592
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a7bdaf1e6de4518d02d1f0bc6af5ddcd39392f4fd8153902ce0414264ec773e2295ad14b8508984ed8140f96742081b1e76f16c2843dd4667e59d25fb558bf3a
|
|
7
|
+
data.tar.gz: d5dfe88b242d5b9ab7d3c27621c1761fd7e5a8a6958dfadcafe20be3e7a89a9f7c33f4b21bcda2574bc29e0ef7ea025b9f905f4d01d9e45c0de2aecc8f22648b
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.4 - 2026-06-16
|
|
4
|
+
|
|
5
|
+
- dependency updates, code quality improvements
|
|
6
|
+
|
|
7
|
+
## 0.4.3 - 2026-06-15
|
|
8
|
+
|
|
9
|
+
- **CapabilityPolicy integration** — CAPABILITY_MAP fed as `:provider_catalog` source. Settings overrides at provider/instance/model level supported.
|
|
10
|
+
|
|
11
|
+
## 0.4.2 — 2026-06-13
|
|
12
|
+
|
|
13
|
+
- **Gemfile cleanup** — Remove local path overrides; dependencies resolve from gemspec via rubygems.
|
|
14
|
+
- **Capabilities** — Add canonical `:tools` to capability declarations.
|
|
15
|
+
- **Bug fix** — Extract nested `cached_tokens` from usage details (G26).
|
|
16
|
+
- 153 examples, 0 failures; 16 files, 0 rubocop offenses.
|
|
17
|
+
|
|
3
18
|
## 0.4.1 — 2026-06-10
|
|
4
19
|
|
|
5
20
|
- Canonical translator (`Translator`): `render_request`, `parse_response`, `parse_chunk`, `capabilities` — provider-boundary contract per N×N routing Amendment A
|
data/Gemfile
CHANGED
|
@@ -2,12 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
source 'https://rubygems.org'
|
|
4
4
|
|
|
5
|
-
group :test do
|
|
6
|
-
transport_path = ENV.fetch('LEGION_TRANSPORT_PATH', File.expand_path('../../legion-transport', __dir__))
|
|
7
|
-
gem 'legion-transport', path: transport_path if File.directory?(transport_path)
|
|
8
|
-
# lex-llm resolved from rubygems (>= 0.5.0) via gemspec - no local path dep
|
|
9
|
-
end
|
|
10
|
-
|
|
11
5
|
gemspec
|
|
12
6
|
|
|
13
7
|
group :development do
|
|
@@ -37,6 +37,13 @@ module Legion
|
|
|
37
37
|
return unless defined?(Legion::LLM::Discovery)
|
|
38
38
|
|
|
39
39
|
Legion::LLM::Discovery.refresh_discovered_models!(provider: :openai)
|
|
40
|
+
|
|
41
|
+
if defined?(Legion::LLM::Router) && Legion::LLM::Router.respond_to?(:populate_auto_rules)
|
|
42
|
+
Legion::LLM::Router.populate_auto_rules(Legion::LLM::Discovery.discovered_instances)
|
|
43
|
+
end
|
|
44
|
+
if defined?(Legion::LLM::Inventory) && Legion::LLM::Inventory.respond_to?(:invalidate_offerings_cache!)
|
|
45
|
+
Legion::LLM::Inventory.invalidate_offerings_cache!
|
|
46
|
+
end
|
|
40
47
|
rescue StandardError => e
|
|
41
48
|
handle_exception(e, level: :warn, handled: true, operation: 'openai.actor.discovery_refresh')
|
|
42
49
|
end
|
|
@@ -17,43 +17,43 @@ module Legion
|
|
|
17
17
|
# the raw /v1/models response.
|
|
18
18
|
CAPABILITY_MAP = {
|
|
19
19
|
'gpt-4o' => {
|
|
20
|
-
capabilities: %i[completion streaming function_calling vision structured_output],
|
|
20
|
+
capabilities: %i[completion streaming function_calling tools vision structured_output],
|
|
21
21
|
modalities_input: %w[text image audio],
|
|
22
22
|
modalities_output: %w[text],
|
|
23
23
|
context_window: 128_000
|
|
24
24
|
},
|
|
25
25
|
'gpt-4.1' => {
|
|
26
|
-
capabilities: %i[completion streaming function_calling vision structured_output],
|
|
26
|
+
capabilities: %i[completion streaming function_calling tools vision structured_output],
|
|
27
27
|
modalities_input: %w[text image],
|
|
28
28
|
modalities_output: %w[text],
|
|
29
29
|
context_window: 1_047_576
|
|
30
30
|
},
|
|
31
31
|
'gpt-4' => {
|
|
32
|
-
capabilities: %i[completion streaming function_calling vision],
|
|
32
|
+
capabilities: %i[completion streaming function_calling tools vision],
|
|
33
33
|
modalities_input: %w[text image],
|
|
34
34
|
modalities_output: %w[text],
|
|
35
35
|
context_window: 128_000
|
|
36
36
|
},
|
|
37
37
|
'gpt-5' => {
|
|
38
|
-
capabilities: %i[completion streaming function_calling vision structured_output reasoning],
|
|
38
|
+
capabilities: %i[completion streaming function_calling tools vision structured_output reasoning],
|
|
39
39
|
modalities_input: %w[text image],
|
|
40
40
|
modalities_output: %w[text],
|
|
41
41
|
context_window: 1_047_576
|
|
42
42
|
},
|
|
43
43
|
'o4' => {
|
|
44
|
-
capabilities: %i[completion streaming function_calling vision reasoning],
|
|
44
|
+
capabilities: %i[completion streaming function_calling tools vision reasoning],
|
|
45
45
|
modalities_input: %w[text image],
|
|
46
46
|
modalities_output: %w[text],
|
|
47
47
|
context_window: 200_000
|
|
48
48
|
},
|
|
49
49
|
'o3' => {
|
|
50
|
-
capabilities: %i[completion streaming function_calling vision reasoning],
|
|
50
|
+
capabilities: %i[completion streaming function_calling tools vision reasoning],
|
|
51
51
|
modalities_input: %w[text image],
|
|
52
52
|
modalities_output: %w[text],
|
|
53
53
|
context_window: 200_000
|
|
54
54
|
},
|
|
55
55
|
'o1' => {
|
|
56
|
-
capabilities: %i[completion streaming function_calling vision reasoning],
|
|
56
|
+
capabilities: %i[completion streaming function_calling tools vision reasoning],
|
|
57
57
|
modalities_input: %w[text image],
|
|
58
58
|
modalities_output: %w[text],
|
|
59
59
|
context_window: 200_000
|
|
@@ -211,6 +211,36 @@ module Legion
|
|
|
211
211
|
raise
|
|
212
212
|
end
|
|
213
213
|
|
|
214
|
+
def discover_offerings(live: false, **)
|
|
215
|
+
models = if live
|
|
216
|
+
@cached_models = list_models
|
|
217
|
+
else
|
|
218
|
+
Array(@cached_models)
|
|
219
|
+
end
|
|
220
|
+
offerings = models.filter_map { |model_info| offering_from_model(model_info) }
|
|
221
|
+
log.debug { "built #{offerings.size} OpenAI offering(s) live=#{live}" }
|
|
222
|
+
offerings
|
|
223
|
+
rescue StandardError => e
|
|
224
|
+
handle_exception(e, level: :warn, handled: true, operation: 'openai.discover_offerings')
|
|
225
|
+
[]
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# ── CapabilityPolicy integration ─────────────────────────────────
|
|
229
|
+
# Maps raw CAPABILITY_MAP symbol arrays to the boolean hash format
|
|
230
|
+
# that CapabilityPolicy.resolve expects as :provider_catalog.
|
|
231
|
+
CATALOG_CAPABILITY_MAPPING = {
|
|
232
|
+
streaming: :streaming,
|
|
233
|
+
function_calling: :tools,
|
|
234
|
+
tools: :tools,
|
|
235
|
+
vision: :vision,
|
|
236
|
+
structured_output: :structured_output,
|
|
237
|
+
reasoning: :thinking,
|
|
238
|
+
embedding: :embeddings,
|
|
239
|
+
image_generation: :image,
|
|
240
|
+
audio_transcription: :audio_transcription,
|
|
241
|
+
audio_generation: :audio_speech
|
|
242
|
+
}.freeze
|
|
243
|
+
|
|
214
244
|
private
|
|
215
245
|
|
|
216
246
|
def build_model_infos(body)
|
|
@@ -248,6 +278,115 @@ module Legion
|
|
|
248
278
|
}
|
|
249
279
|
end
|
|
250
280
|
|
|
281
|
+
def offering_from_model(model_info)
|
|
282
|
+
policy = resolve_model_policy(model_info)
|
|
283
|
+
|
|
284
|
+
Legion::Extensions::Llm::Routing::ModelOffering.new(
|
|
285
|
+
offering_attrs_for(model_info, policy)
|
|
286
|
+
)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def resolve_model_policy(model_info)
|
|
290
|
+
catalog = capabilities_to_boolean_hash(capability_entry_for(model_info.id)[:capabilities])
|
|
291
|
+
|
|
292
|
+
Legion::Extensions::Llm::CapabilityPolicy.resolve(
|
|
293
|
+
real: {},
|
|
294
|
+
provider_catalog: catalog,
|
|
295
|
+
probe: {},
|
|
296
|
+
provider_envelope: {},
|
|
297
|
+
provider_config: provider_capability_config,
|
|
298
|
+
instance_config: instance_capability_config,
|
|
299
|
+
model_config: model_capability_config(model_info.id)
|
|
300
|
+
)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def offering_attrs_for(model_info, policy)
|
|
304
|
+
{
|
|
305
|
+
provider_family: :openai,
|
|
306
|
+
instance_id: config.respond_to?(:instance_id) ? config.instance_id : :default,
|
|
307
|
+
transport: :http,
|
|
308
|
+
tier: :frontier,
|
|
309
|
+
model: model_info.id,
|
|
310
|
+
canonical_model_alias: model_info.respond_to?(:name) ? model_info.name : nil,
|
|
311
|
+
model_family: infer_model_family(model_info.id),
|
|
312
|
+
usage_type: infer_usage_type(model_info),
|
|
313
|
+
capabilities: policy[:capabilities],
|
|
314
|
+
capability_sources: policy[:sources],
|
|
315
|
+
limits: { context_window: model_info.context_length }.compact,
|
|
316
|
+
metadata: { capability_sources: policy[:sources] }
|
|
317
|
+
}
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def capabilities_to_boolean_hash(capability_symbols)
|
|
321
|
+
return {} unless capability_symbols.is_a?(Array)
|
|
322
|
+
|
|
323
|
+
result = {}
|
|
324
|
+
capability_symbols.each do |sym|
|
|
325
|
+
mapped = CATALOG_CAPABILITY_MAPPING[sym]
|
|
326
|
+
result[mapped] = true if mapped
|
|
327
|
+
end
|
|
328
|
+
result
|
|
329
|
+
end
|
|
330
|
+
|
|
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
|
+
def infer_model_family(model_id)
|
|
375
|
+
CAPABILITY_MAP.each_key do |prefix|
|
|
376
|
+
return prefix.tr('-', '_').to_sym if model_id.start_with?(prefix)
|
|
377
|
+
end
|
|
378
|
+
:unknown
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def infer_usage_type(model_info)
|
|
382
|
+
caps = model_info.respond_to?(:capabilities) ? Array(model_info.capabilities) : []
|
|
383
|
+
return :embedding if caps.include?(:embedding)
|
|
384
|
+
return :moderation if caps.include?(:moderation)
|
|
385
|
+
return :image if caps.include?(:image_generation)
|
|
386
|
+
|
|
387
|
+
:inference
|
|
388
|
+
end
|
|
389
|
+
|
|
251
390
|
def fetch_model_detail(model_name)
|
|
252
391
|
entry = capability_entry_for(model_name)
|
|
253
392
|
ctx = entry[:context_window]
|
|
@@ -287,7 +287,8 @@ module Legion
|
|
|
287
287
|
end
|
|
288
288
|
|
|
289
289
|
def render_openai_tool_calls(tool_calls)
|
|
290
|
-
|
|
290
|
+
tc_array = tool_calls.is_a?(Hash) ? tool_calls.values : Array(tool_calls)
|
|
291
|
+
tc_array.map do |tc|
|
|
291
292
|
args = tc.arguments.is_a?(String) ? tc.arguments : Legion::JSON.generate(tc.arguments || {})
|
|
292
293
|
|
|
293
294
|
{
|
|
@@ -353,7 +354,24 @@ module Legion
|
|
|
353
354
|
def parse_usage(raw)
|
|
354
355
|
return nil unless raw&.any?
|
|
355
356
|
|
|
356
|
-
|
|
357
|
+
normalized = raw.dup
|
|
358
|
+
normalized[:cache_read_tokens] ||= extract_nested_cached_tokens(raw)
|
|
359
|
+
normalized[:thinking_tokens] ||= extract_nested_reasoning_tokens(raw)
|
|
360
|
+
Canonical::Usage.from_hash(normalized)
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def extract_nested_cached_tokens(raw)
|
|
364
|
+
raw.dig(:prompt_tokens_details, :cached_tokens) ||
|
|
365
|
+
raw.dig('prompt_tokens_details', 'cached_tokens') ||
|
|
366
|
+
raw.dig(:input_tokens_details, :cached_tokens) ||
|
|
367
|
+
raw.dig('input_tokens_details', 'cached_tokens')
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def extract_nested_reasoning_tokens(raw)
|
|
371
|
+
raw.dig(:completion_tokens_details, :reasoning_tokens) ||
|
|
372
|
+
raw.dig('completion_tokens_details', 'reasoning_tokens') ||
|
|
373
|
+
raw.dig(:output_tokens_details, :reasoning_tokens) ||
|
|
374
|
+
raw.dig('output_tokens_details', 'reasoning_tokens')
|
|
357
375
|
end
|
|
358
376
|
|
|
359
377
|
def map_stop_reason(raw)
|
|
@@ -4,6 +4,7 @@ require 'legion/extensions/llm'
|
|
|
4
4
|
require 'legion/extensions/llm/openai/provider'
|
|
5
5
|
require 'legion/extensions/llm/openai/translator'
|
|
6
6
|
require 'legion/extensions/llm/openai/version'
|
|
7
|
+
require_relative 'openai/actors/discovery_refresh'
|
|
7
8
|
|
|
8
9
|
module Legion
|
|
9
10
|
module Extensions
|
|
@@ -40,10 +41,7 @@ module Legion
|
|
|
40
41
|
fleet: {
|
|
41
42
|
enabled: false,
|
|
42
43
|
respond_to_requests: false,
|
|
43
|
-
capabilities: %i[chat stream_chat embed image]
|
|
44
|
-
lanes: [],
|
|
45
|
-
concurrency: 4,
|
|
46
|
-
queue_suffix: nil
|
|
44
|
+
capabilities: %i[chat stream_chat embed image]
|
|
47
45
|
}
|
|
48
46
|
}
|
|
49
47
|
)
|
|
@@ -135,8 +133,7 @@ module Legion
|
|
|
135
133
|
config.except(:api_key, :organization_id, :project_id)
|
|
136
134
|
end
|
|
137
135
|
|
|
138
|
-
Legion::Extensions::Llm::Configuration.register_provider_options(Provider.configuration_options)
|
|
139
|
-
Legion::Extensions::Llm::Configuration.respond_to?(:register_provider_options)
|
|
136
|
+
Legion::Extensions::Llm::Configuration.register_provider_options(Provider.configuration_options)
|
|
140
137
|
end
|
|
141
138
|
end
|
|
142
139
|
end
|