lex-openai 0.1.3 → 0.1.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: b8f5858ed3e18e95299a0e2c0df9069c4a96bff7bb1a72051dcc0e6e9ae0265a
4
- data.tar.gz: bd6977abff97ae0e1428b3f2c2dace21a1c8bd817f3b2814ba46531f8cf8781c
3
+ metadata.gz: 90c6d63dfd5f751c914a536a7392e0d8ddfb9e8c50b06b29d714c9a27f20d958
4
+ data.tar.gz: 734c7f7eb7089efecbe0217b75f1bde7d2203593bd7298715024e1fb12ed4a5b
5
5
  SHA512:
6
- metadata.gz: e093b207dc65fee0d62c6b17a8dd75b00b11a307ec893540f79942e522115c7f8eb8d6cafbbfdb2cbf54ea1ff999f16c61fed2d35e58a036bf4f024ebd6c92ec
7
- data.tar.gz: e4b5f51079d5ea17effab271dd879665cf2fc03f2678fbd3f9f1d8d3504a05f235b825f6cdc99fd491a2b80ae751b11994e0355d09b117a8249c595ed80445b2
6
+ metadata.gz: 6d85f2b69e108f1b81b8596aed84763f66e9a02b54e981ac2228791ff017ca44238e3afc8d906e3effb47ea6c9217dfc7785b46bb04792cf7fc5bb9bf534c621
7
+ data.tar.gz: b7b973d63e125b00d5503aadcaca25127cc5529779022671af6c9fb2259de630336450cd48243e88cd0169e9c3c0973f978a3423760ec0fa4f6ac3f152eb924f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.5] - 2026-04-06
4
+
5
+ ### Added
6
+ - Credential-only identity module for Phase 8 Broker integration (`Identity` module with `provide_token`)
7
+
8
+ ## [0.1.4] - 2026-03-31
9
+
10
+ ### Added
11
+ - add standardized usage tracking to all runner responses; all methods now return a `usage:` hash with `input_tokens`, `output_tokens`, `cache_read_tokens`, and `cache_write_tokens` keys compatible with legion-llm's CostEstimator (#2)
12
+
3
13
  ## [0.1.3] - 2026-03-30
4
14
 
5
15
  ### Changed
data/CLAUDE.md CHANGED
@@ -10,8 +10,8 @@ Legion Extension that connects LegionIO to OpenAI. Provides runners for chat com
10
10
 
11
11
  **GitHub**: https://github.com/LegionIO/lex-openai
12
12
  **License**: MIT
13
- **Version**: 0.1.2
14
- **Specs**: 17 examples
13
+ **Version**: 0.1.5
14
+ **Specs**: 66 examples (8 spec files)
15
15
 
16
16
  ## Architecture
17
17
 
@@ -31,15 +31,16 @@ Legion::Extensions::Openai
31
31
 
32
32
  There is no standalone `Client` class in lex-openai. Runner modules are used directly via `extend` or by including them in a consuming class. This differs from lex-azure-ai, lex-bedrock, lex-claude, lex-foundry, and lex-xai which all ship a `Client` class.
33
33
 
34
- `Helpers::Client` is a **module** (not a class). It does not use `module_function` — instead, runner modules `extend` it so `client(...)` is available as a module-level method. `DEFAULT_BASE_URL` is `'https://api.openai.com'`.
34
+ `Helpers::Client` is a **module** (not a class). Runner modules `extend` it so `client(...)` is available as a module-level method. `DEFAULT_BASE_URL = 'https://api.openai.com'`.
35
35
 
36
36
  ## Key Design Decisions
37
37
 
38
38
  - `faraday/multipart` is required unconditionally in `Helpers::Client` — the `:multipart` middleware is always loaded. This is a hard dependency (listed in gemspec), unlike lex-gemini where it is optional.
39
- - Images (edit, variation) and Audio (transcribe, translate) runners use `Faraday::Multipart::FilePart` directly.
39
+ - `Images#edit` and `Images#variation` use `Faraday::Multipart::FilePart` directly.
40
40
  - `Images#generate` uses DALL-E 3 by default; `Images#edit` and `Images#variation` use DALL-E 2 by default.
41
41
  - Audio defaults: `model: 'tts-1'`, `voice: 'alloy'`, `response_format: 'mp3'` for speech; `model: 'whisper-1'` for transcription/translation.
42
- - All runners return `{ result: response.body }` (no `:status` key). This is consistent with lex-gemini runners. lex-claude runners add `:status` to the return hash.
42
+ - All runners return `{ result: response.body }` (no `:status` key).
43
+ - `multi_json` is NOT a declared dependency of lex-openai (unlike lex-azure-ai, lex-claude, lex-foundry, lex-xai). JSON parsing uses Faraday's built-in response middleware.
43
44
  - `include Legion::Extensions::Helpers::Lex` is guarded with `Legion::Extensions.const_defined?(:Helpers)` pattern.
44
45
 
45
46
  ## Dependencies
@@ -48,17 +49,19 @@ There is no standalone `Client` class in lex-openai. Runner modules are used dir
48
49
  |-----|---------|
49
50
  | `faraday` >= 2.0 | HTTP client |
50
51
  | `faraday-multipart` >= 1.0 | Multipart file uploads (images, audio, files) |
52
+ | `legion-cache`, `legion-crypt`, `legion-data`, `legion-json`, `legion-logging`, `legion-settings`, `legion-transport` | LegionIO core |
51
53
 
52
- Note: `multi_json` is NOT a declared dependency of lex-openai (unlike the other extensions in this category). JSON parsing uses Faraday's built-in response middleware.
54
+ Note: `multi_json` is NOT a declared dependency (differs from all other extensions in this category).
53
55
 
54
56
  ## Testing
55
57
 
56
58
  ```bash
57
59
  bundle install
58
- bundle exec rspec # 17 examples
60
+ bundle exec rspec # 66 examples
59
61
  bundle exec rubocop
60
62
  ```
61
63
 
62
64
  ---
63
65
 
64
66
  **Maintained By**: Matthew Iverson (@Esity)
67
+ **Last Updated**: 2026-04-06
data/README.md CHANGED
@@ -100,7 +100,6 @@ puts image[:result]['data'].first['url']
100
100
 
101
101
  - `faraday` >= 2.0 - HTTP client
102
102
  - `faraday-multipart` >= 1.0 - Multipart file uploads (images, audio, files)
103
- - `multi_json` - JSON parser abstraction
104
103
 
105
104
  ## Requirements
106
105
 
@@ -114,6 +113,10 @@ puts image[:result]['data'].first['url']
114
113
  - `legion-llm` — High-level LLM interface including OpenAI via ruby_llm
115
114
  - `extensions-ai/CLAUDE.md` — Architecture patterns shared across all AI extensions
116
115
 
116
+ ## Version
117
+
118
+ 0.1.5
119
+
117
120
  ## License
118
121
 
119
122
  MIT
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Openai
6
+ module Identity
7
+ module_function
8
+
9
+ def provider_name = :openai
10
+ def provider_type = :credential
11
+ def facing = nil
12
+ def capabilities = %i[credentials]
13
+
14
+ def resolve(canonical_name: nil) # rubocop:disable Lint/UnusedMethodArgument
15
+ nil
16
+ end
17
+
18
+ def provide_token
19
+ api_key = resolve_api_key
20
+ return nil unless api_key
21
+
22
+ Legion::Identity::Lease.new(
23
+ provider: :openai,
24
+ credential: api_key,
25
+ expires_at: nil,
26
+ renewable: false,
27
+ issued_at: Time.now,
28
+ metadata: { credential_type: :api_key }
29
+ )
30
+ end
31
+
32
+ def resolve_api_key
33
+ return nil unless defined?(Legion::Settings)
34
+
35
+ value = Legion::Settings.dig(:llm, :providers, :openai, :api_key)
36
+ value = value.find { |v| v && !v.empty? } if value.is_a?(Array)
37
+ value unless value.nil? || (value.is_a?(String) && (value.empty? || value.start_with?('env://')))
38
+ end
39
+
40
+ private_class_method :resolve_api_key
41
+ end
42
+ end
43
+ end
44
+ end
@@ -14,7 +14,15 @@ module Legion
14
14
  body[:speed] = speed if speed
15
15
 
16
16
  response = client(api_key: api_key, **).post('/v1/audio/speech', body)
17
- { result: response.body }
17
+ {
18
+ result: response.body,
19
+ usage: {
20
+ input_tokens: 0,
21
+ output_tokens: 0,
22
+ cache_read_tokens: 0,
23
+ cache_write_tokens: 0
24
+ }
25
+ }
18
26
  end
19
27
 
20
28
  def transcribe(file:, api_key:, model: 'whisper-1', language: nil, prompt: nil, response_format: nil, **)
@@ -27,7 +35,15 @@ module Legion
27
35
  payload[:response_format] = response_format if response_format
28
36
 
29
37
  response = client(api_key: api_key, **).post('/v1/audio/transcriptions', payload)
30
- { result: response.body }
38
+ {
39
+ result: response.body,
40
+ usage: {
41
+ input_tokens: 0,
42
+ output_tokens: 0,
43
+ cache_read_tokens: 0,
44
+ cache_write_tokens: 0
45
+ }
46
+ }
31
47
  end
32
48
 
33
49
  def translate(file:, api_key:, model: 'whisper-1', prompt: nil, response_format: nil, **)
@@ -39,7 +55,15 @@ module Legion
39
55
  payload[:response_format] = response_format if response_format
40
56
 
41
57
  response = client(api_key: api_key, **).post('/v1/audio/translations', payload)
42
- { result: response.body }
58
+ {
59
+ result: response.body,
60
+ usage: {
61
+ input_tokens: 0,
62
+ output_tokens: 0,
63
+ cache_read_tokens: 0,
64
+ cache_write_tokens: 0
65
+ }
66
+ }
43
67
  end
44
68
 
45
69
  include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
@@ -20,7 +20,16 @@ module Legion
20
20
  body[:stop] = stop if stop
21
21
 
22
22
  response = client(api_key: api_key, **).post('/v1/chat/completions', body)
23
- { result: response.body }
23
+ resp_body = response.body
24
+ {
25
+ result: resp_body,
26
+ usage: {
27
+ input_tokens: resp_body.dig('usage', 'prompt_tokens') || 0,
28
+ output_tokens: resp_body.dig('usage', 'completion_tokens') || 0,
29
+ cache_read_tokens: resp_body.dig('usage', 'prompt_tokens_details', 'cached_tokens') || 0,
30
+ cache_write_tokens: 0
31
+ }
32
+ }
24
33
  end
25
34
 
26
35
  include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
@@ -15,7 +15,16 @@ module Legion
15
15
  body[:dimensions] = dimensions if dimensions
16
16
 
17
17
  response = client(api_key: api_key, **).post('/v1/embeddings', body)
18
- { result: response.body }
18
+ resp_body = response.body
19
+ {
20
+ result: resp_body,
21
+ usage: {
22
+ input_tokens: resp_body.dig('usage', 'prompt_tokens') || 0,
23
+ output_tokens: resp_body.dig('usage', 'completion_tokens') || 0,
24
+ cache_read_tokens: resp_body.dig('usage', 'prompt_tokens_details', 'cached_tokens') || 0,
25
+ cache_write_tokens: 0
26
+ }
27
+ }
19
28
  end
20
29
 
21
30
  include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
@@ -14,7 +14,15 @@ module Legion
14
14
  path += "?purpose=#{purpose}" if purpose
15
15
 
16
16
  response = client(api_key: api_key, **).get(path)
17
- { result: response.body }
17
+ {
18
+ result: response.body,
19
+ usage: {
20
+ input_tokens: 0,
21
+ output_tokens: 0,
22
+ cache_read_tokens: 0,
23
+ cache_write_tokens: 0
24
+ }
25
+ }
18
26
  end
19
27
 
20
28
  def upload(file:, purpose:, api_key:, **)
@@ -24,22 +32,54 @@ module Legion
24
32
  }
25
33
 
26
34
  response = client(api_key: api_key, **).post('/v1/files', payload)
27
- { result: response.body }
35
+ {
36
+ result: response.body,
37
+ usage: {
38
+ input_tokens: 0,
39
+ output_tokens: 0,
40
+ cache_read_tokens: 0,
41
+ cache_write_tokens: 0
42
+ }
43
+ }
28
44
  end
29
45
 
30
46
  def retrieve(file_id:, api_key:, **)
31
47
  response = client(api_key: api_key, **).get("/v1/files/#{file_id}")
32
- { result: response.body }
48
+ {
49
+ result: response.body,
50
+ usage: {
51
+ input_tokens: 0,
52
+ output_tokens: 0,
53
+ cache_read_tokens: 0,
54
+ cache_write_tokens: 0
55
+ }
56
+ }
33
57
  end
34
58
 
35
59
  def delete(file_id:, api_key:, **)
36
60
  response = client(api_key: api_key, **).delete("/v1/files/#{file_id}")
37
- { result: response.body }
61
+ {
62
+ result: response.body,
63
+ usage: {
64
+ input_tokens: 0,
65
+ output_tokens: 0,
66
+ cache_read_tokens: 0,
67
+ cache_write_tokens: 0
68
+ }
69
+ }
38
70
  end
39
71
 
40
72
  def content(file_id:, api_key:, **)
41
73
  response = client(api_key: api_key, **).get("/v1/files/#{file_id}/content")
42
- { result: response.body }
74
+ {
75
+ result: response.body,
76
+ usage: {
77
+ input_tokens: 0,
78
+ output_tokens: 0,
79
+ cache_read_tokens: 0,
80
+ cache_write_tokens: 0
81
+ }
82
+ }
43
83
  end
44
84
 
45
85
  include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
@@ -17,7 +17,15 @@ module Legion
17
17
  body[:response_format] = response_format if response_format
18
18
 
19
19
  response = client(api_key: api_key, **).post('/v1/images/generations', body)
20
- { result: response.body }
20
+ {
21
+ result: response.body,
22
+ usage: {
23
+ input_tokens: 0,
24
+ output_tokens: 0,
25
+ cache_read_tokens: 0,
26
+ cache_write_tokens: 0
27
+ }
28
+ }
21
29
  end
22
30
 
23
31
  def edit(image:, prompt:, api_key:, model: 'dall-e-2', mask: nil, n: 1, size: '1024x1024', **)
@@ -31,7 +39,15 @@ module Legion
31
39
  payload[:mask] = Faraday::Multipart::FilePart.new(mask, 'image/png') if mask
32
40
 
33
41
  response = client(api_key: api_key, **).post('/v1/images/edits', payload)
34
- { result: response.body }
42
+ {
43
+ result: response.body,
44
+ usage: {
45
+ input_tokens: 0,
46
+ output_tokens: 0,
47
+ cache_read_tokens: 0,
48
+ cache_write_tokens: 0
49
+ }
50
+ }
35
51
  end
36
52
 
37
53
  def variation(image:, api_key:, model: 'dall-e-2', n: 1, size: '1024x1024', **)
@@ -43,7 +59,15 @@ module Legion
43
59
  }
44
60
 
45
61
  response = client(api_key: api_key, **).post('/v1/images/variations', payload)
46
- { result: response.body }
62
+ {
63
+ result: response.body,
64
+ usage: {
65
+ input_tokens: 0,
66
+ output_tokens: 0,
67
+ cache_read_tokens: 0,
68
+ cache_write_tokens: 0
69
+ }
70
+ }
47
71
  end
48
72
 
49
73
  include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
@@ -11,17 +11,41 @@ module Legion
11
11
 
12
12
  def list(api_key:, **)
13
13
  response = client(api_key: api_key, **).get('/v1/models')
14
- { result: response.body }
14
+ {
15
+ result: response.body,
16
+ usage: {
17
+ input_tokens: 0,
18
+ output_tokens: 0,
19
+ cache_read_tokens: 0,
20
+ cache_write_tokens: 0
21
+ }
22
+ }
15
23
  end
16
24
 
17
25
  def retrieve(model:, api_key:, **)
18
26
  response = client(api_key: api_key, **).get("/v1/models/#{model}")
19
- { result: response.body }
27
+ {
28
+ result: response.body,
29
+ usage: {
30
+ input_tokens: 0,
31
+ output_tokens: 0,
32
+ cache_read_tokens: 0,
33
+ cache_write_tokens: 0
34
+ }
35
+ }
20
36
  end
21
37
 
22
38
  def delete(model:, api_key:, **)
23
39
  response = client(api_key: api_key, **).delete("/v1/models/#{model}")
24
- { result: response.body }
40
+ {
41
+ result: response.body,
42
+ usage: {
43
+ input_tokens: 0,
44
+ output_tokens: 0,
45
+ cache_read_tokens: 0,
46
+ cache_write_tokens: 0
47
+ }
48
+ }
25
49
  end
26
50
 
27
51
  include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
@@ -14,7 +14,16 @@ module Legion
14
14
  body[:model] = model if model
15
15
 
16
16
  response = client(api_key: api_key, **).post('/v1/moderations', body)
17
- { result: response.body }
17
+ resp_body = response.body
18
+ {
19
+ result: resp_body,
20
+ usage: {
21
+ input_tokens: resp_body.dig('usage', 'prompt_tokens') || 0,
22
+ output_tokens: resp_body.dig('usage', 'completion_tokens') || 0,
23
+ cache_read_tokens: resp_body.dig('usage', 'prompt_tokens_details', 'cached_tokens') || 0,
24
+ cache_write_tokens: 0
25
+ }
26
+ }
18
27
  end
19
28
 
20
29
  include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module Openai
6
- VERSION = '0.1.3'
6
+ VERSION = '0.1.5'
7
7
  end
8
8
  end
9
9
  end
@@ -9,6 +9,7 @@ require 'legion/extensions/openai/runners/audio'
9
9
  require 'legion/extensions/openai/runners/embeddings'
10
10
  require 'legion/extensions/openai/runners/files'
11
11
  require 'legion/extensions/openai/runners/moderations'
12
+ require 'legion/extensions/openai/identity'
12
13
 
13
14
  module Legion
14
15
  module Extensions
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-openai
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -157,6 +157,7 @@ files:
157
157
  - lex-openai.gemspec
158
158
  - lib/legion/extensions/openai.rb
159
159
  - lib/legion/extensions/openai/helpers/client.rb
160
+ - lib/legion/extensions/openai/identity.rb
160
161
  - lib/legion/extensions/openai/runners/audio.rb
161
162
  - lib/legion/extensions/openai/runners/chat.rb
162
163
  - lib/legion/extensions/openai/runners/embeddings.rb