lex-llm-openai 0.4.1 → 0.4.5

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: 4091bfdcfbcd60f52453e29e42e30a4564edd48dabcd9b0f8edadfa68440f52b
4
+ data.tar.gz: 0de0b799f80f47f937a9ecdc797996c4c0d3e64784fc518bf0b5de8628df135b
5
5
  SHA512:
6
- metadata.gz: 4e3886489304539fda6fe6566aa7f596682bcd480944822eebf473ff638a0c3f47b8f88243444afaf3e2ccd58a6b1699c4918b627eb859fcef7a409884536294
7
- data.tar.gz: efaaea62fada60fe7f598426c59028d6461ad6afb5c935d9e3a77b43b22a6d9ae0e76577ae75a9b5e96b40f6f9bff8cc1dbf881a082fab3a88c93f00516bfd67
6
+ metadata.gz: 0abdd81c00439b7ccba758adbd7f8576e83ce5e367673ce2092708dc3f61290c406db462e0736e56402957c32f47b0fd781bdcf6486508a5b8486ab48f6d2edf
7
+ data.tar.gz: 29b4b768e0bd91aee7cd05283b62caf93d4ffbd6215063c7fa480d3ed1a2b164341e669d6badcb4e7f804db55e9500e81c016eef5f4a7223c20039730942ec37
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.5 - 2026-06-17
4
+
5
+ ### Changed
6
+ - **Policy-aware default model** — `default_model` is no longer a hardcoded literal forced via `||=`. The `gpt-5.5` fallback is now a named `DEFAULT_MODEL` constant applied through `Provider.policy_safe_default_model`, so a configured `model_whitelist`/`model_blacklist` is never overridden: if neither the configured default nor the fallback is permitted, `default_model` is left unset and routing resolves an allowed discovered model instead. Requires lex-llm >= 0.5.4.
7
+
8
+ ## 0.4.4 - 2026-06-16
9
+
10
+ - dependency updates, code quality improvements
11
+
12
+ ## 0.4.3 - 2026-06-15
13
+
14
+ - **CapabilityPolicy integration** — CAPABILITY_MAP fed as `:provider_catalog` source. Settings overrides at provider/instance/model level supported.
15
+
16
+ ## 0.4.2 — 2026-06-13
17
+
18
+ - **Gemfile cleanup** — Remove local path overrides; dependencies resolve from gemspec via rubygems.
19
+ - **Capabilities** — Add canonical `:tools` to capability declarations.
20
+ - **Bug fix** — Extract nested `cached_tokens` from usage details (G26).
21
+ - 153 examples, 0 failures; 16 files, 0 rubocop offenses.
22
+
3
23
  ## 0.4.1 — 2026-06-10
4
24
 
5
25
  - 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
@@ -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.5.0'
30
+ spec.add_dependency 'lex-llm', '>= 0.5.4'
31
31
  end
@@ -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.5'
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
@@ -15,13 +16,17 @@ module Legion
15
16
  extend Legion::Extensions::Llm::AutoRegistration
16
17
 
17
18
  PROVIDER_FAMILY = :openai
19
+ # Provider's preferred default when the operator configures none. Used only
20
+ # as a fallback and only when the configured model policy permits it
21
+ # (see resolve_default_model) — a whitelist/blacklist is never overridden.
22
+ DEFAULT_MODEL = 'gpt-5.5'
18
23
 
19
24
  def self.default_settings
20
25
  ::Legion::Extensions::Llm.provider_settings(
21
26
  family: PROVIDER_FAMILY,
22
27
  instance: {
23
28
  endpoint: 'https://api.openai.com',
24
- default_model: 'gpt-5.5',
29
+ default_model: DEFAULT_MODEL,
25
30
  tier: :frontier,
26
31
  transport: :http,
27
32
  credentials: {
@@ -40,10 +45,7 @@ module Legion
40
45
  fleet: {
41
46
  enabled: false,
42
47
  respond_to_requests: false,
43
- capabilities: %i[chat stream_chat embed image],
44
- lanes: [],
45
- concurrency: 4,
46
- queue_suffix: nil
48
+ capabilities: %i[chat stream_chat embed image]
47
49
  }
48
50
  }
49
51
  )
@@ -102,10 +104,10 @@ module Legion
102
104
  candidates[name.to_sym] = normalized.merge(tier: :frontier)
103
105
  end
104
106
 
105
- # 8. Dedup + inject default_model
107
+ # 8. Dedup + inject a policy-aware default_model
106
108
  discovered = CredentialSources.dedup_credentials(candidates).transform_values do |config|
107
109
  sanitized = sanitize_instance_config(config)
108
- sanitized[:default_model] ||= 'gpt-5.5'
110
+ sanitized[:default_model] = resolve_default_model(sanitized)
109
111
  sanitized
110
112
  end
111
113
  instance_names = discovered.keys.sort_by(&:to_s).join(', ')
@@ -113,6 +115,15 @@ module Legion
113
115
  discovered
114
116
  end
115
117
 
118
+ # Resolve a default_model that never violates the configured model policy
119
+ # (whitelist/blacklist stays authoritative over the DEFAULT_MODEL fallback).
120
+ def self.resolve_default_model(config)
121
+ provider_class.policy_safe_default_model(
122
+ configured: config[:default_model], fallback: DEFAULT_MODEL,
123
+ **provider_class.model_policy(config, PROVIDER_FAMILY)
124
+ )
125
+ end
126
+
116
127
  def self.settings_instances(config)
117
128
  return {} unless config.is_a?(Hash)
118
129
 
@@ -135,8 +146,7 @@ module Legion
135
146
  config.except(:api_key, :organization_id, :project_id)
136
147
  end
137
148
 
138
- Legion::Extensions::Llm::Configuration.register_provider_options(Provider.configuration_options) if
139
- Legion::Extensions::Llm::Configuration.respond_to?(:register_provider_options)
149
+ Legion::Extensions::Llm::Configuration.register_provider_options(Provider.configuration_options)
140
150
  end
141
151
  end
142
152
  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.5
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.5.0
74
+ version: 0.5.4
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.5.0
81
+ version: 0.5.4
82
82
  description: OpenAI provider integration for the LegionIO LLM routing framework.
83
83
  email:
84
84
  - matthewdiverson@gmail.com