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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1e0bcb314dd98bb72b108f3f92a05ceb40dd8fc125e3972712674537e5baa2a9
4
- data.tar.gz: b9fd92e6d25f2e9949fa5b5e1a7d79f8d9ed04a197b873a0b754d309dc30aef6
3
+ metadata.gz: 7bb7451045c6e85e6e89657665065fe502888f49982d3dc43a53fdcb1bf4ebcd
4
+ data.tar.gz: 05e91bb97ec042142b126b310e9e05dcfb99fe1ae999b8595f7924102106d592
5
5
  SHA512:
6
- metadata.gz: 4e3886489304539fda6fe6566aa7f596682bcd480944822eebf473ff638a0c3f47b8f88243444afaf3e2ccd58a6b1699c4918b627eb859fcef7a409884536294
7
- data.tar.gz: efaaea62fada60fe7f598426c59028d6461ad6afb5c935d9e3a77b43b22a6d9ae0e76577ae75a9b5e96b40f6f9bff8cc1dbf881a082fab3a88c93f00516bfd67
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
- Array(tool_calls).map do |tc|
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
- Canonical::Usage.from_hash(raw)
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,7 +4,7 @@ module Legion
4
4
  module Extensions
5
5
  module Llm
6
6
  module Openai
7
- VERSION = '0.4.1'
7
+ VERSION = '0.4.4'
8
8
  end
9
9
  end
10
10
  end
@@ -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) if
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
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.1
4
+ version: 0.4.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - LegionIO