lex-llm 0.4.15 → 0.4.18
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/.rubocop.yml.new +54 -0
- data/CHANGELOG.md +27 -0
- data/README.md +349 -153
- data/lex-llm.gemspec +1 -0
- data/lib/legion/extensions/llm/configuration.rb +4 -0
- data/lib/legion/extensions/llm/connection.rb +10 -1
- data/lib/legion/extensions/llm/credential_sources.rb +14 -6
- data/lib/legion/extensions/llm/fleet/token_validator.rb +28 -5
- data/lib/legion/extensions/llm/fleet/worker_execution.rb +13 -2
- data/lib/legion/extensions/llm/model/info.rb +7 -1
- data/lib/legion/extensions/llm/models.json +138 -66
- data/lib/legion/extensions/llm/provider/open_ai_compatible.rb +5 -1
- data/lib/legion/extensions/llm/provider.rb +14 -0
- data/lib/legion/extensions/llm/streaming.rb +5 -1
- data/lib/legion/extensions/llm/transport/messages/fleet_error.rb +1 -0
- data/lib/legion/extensions/llm/transport/messages/fleet_request.rb +1 -0
- data/lib/legion/extensions/llm/transport/messages/fleet_response.rb +1 -0
- data/lib/legion/extensions/llm/version.rb +1 -1
- metadata +16 -1
|
@@ -42,6 +42,7 @@ module Legion
|
|
|
42
42
|
|
|
43
43
|
ensure_configured!
|
|
44
44
|
@connection ||= Faraday.new(provider.api_base) do |faraday|
|
|
45
|
+
faraday.ssl.verify = false
|
|
45
46
|
setup_timeout(faraday)
|
|
46
47
|
setup_logging(faraday)
|
|
47
48
|
setup_retry(faraday)
|
|
@@ -82,6 +83,7 @@ module Legion
|
|
|
82
83
|
errors: false,
|
|
83
84
|
headers: false,
|
|
84
85
|
log_level: :debug do |logger|
|
|
86
|
+
logger.filter(logging_regexp('Bearer [A-Za-z0-9._\\-]+'), 'Bearer [REDACTED]')
|
|
85
87
|
logger.filter(logging_regexp('[A-Za-z0-9+/=]{100,}'), '[BASE64 DATA]')
|
|
86
88
|
logger.filter(logging_regexp('[-\\d.e,\\s]{100,}'), '[EMBEDDINGS ARRAY]')
|
|
87
89
|
end
|
|
@@ -121,7 +123,13 @@ module Legion
|
|
|
121
123
|
faraday.request :multipart
|
|
122
124
|
faraday.request :json
|
|
123
125
|
faraday.response :json
|
|
124
|
-
|
|
126
|
+
# Prefer typhoeus (libcurl) over net_http to avoid Ruby 4.0 + net-http-0.9.1 SSL issues
|
|
127
|
+
begin
|
|
128
|
+
require 'faraday/typhoeus'
|
|
129
|
+
faraday.adapter :typhoeus
|
|
130
|
+
rescue LoadError
|
|
131
|
+
faraday.adapter :net_http
|
|
132
|
+
end
|
|
125
133
|
faraday.use :llm_errors, provider: @provider
|
|
126
134
|
end
|
|
127
135
|
|
|
@@ -137,6 +145,7 @@ module Legion
|
|
|
137
145
|
Timeout::Error,
|
|
138
146
|
Faraday::TimeoutError,
|
|
139
147
|
Faraday::ConnectionFailed,
|
|
148
|
+
Faraday::SSLError,
|
|
140
149
|
Faraday::RetriableResponse,
|
|
141
150
|
Legion::Extensions::Llm::RateLimitError,
|
|
142
151
|
Legion::Extensions::Llm::ServerError,
|
|
@@ -18,6 +18,12 @@ module Legion
|
|
|
18
18
|
CLAUDE_PROJECT = File.join(Dir.pwd, '.claude', 'settings.json')
|
|
19
19
|
CODEX_AUTH = File.expand_path('~/.codex/auth.json')
|
|
20
20
|
|
|
21
|
+
def credential_source_probing_enabled?
|
|
22
|
+
return true unless defined?(::Legion::Settings)
|
|
23
|
+
|
|
24
|
+
::Legion::Settings.dig(:extensions, :llm, :security, :credential_source_probing) != false
|
|
25
|
+
end
|
|
26
|
+
|
|
21
27
|
# --- public helpers ------------------------------------------------
|
|
22
28
|
|
|
23
29
|
# Fetch an environment variable, stripping whitespace.
|
|
@@ -30,9 +36,9 @@ module Legion
|
|
|
30
36
|
stripped.empty? ? nil : stripped
|
|
31
37
|
end
|
|
32
38
|
|
|
33
|
-
# Merged Claude config (user-level + project-level). Project settings
|
|
34
|
-
# override user settings. Memoized for the lifetime of the process.
|
|
35
39
|
def claude_config
|
|
40
|
+
return {} unless credential_source_probing_enabled?
|
|
41
|
+
|
|
36
42
|
@claude_config ||= merge_claude_configs
|
|
37
43
|
end
|
|
38
44
|
|
|
@@ -52,9 +58,9 @@ module Legion
|
|
|
52
58
|
env_hash[key.to_sym] || env_hash[key.to_s]
|
|
53
59
|
end
|
|
54
60
|
|
|
55
|
-
# Read the bearer token from ~/.codex/auth.json when auth_mode is
|
|
56
|
-
# "chatgpt" and the JWT is not expired.
|
|
57
61
|
def codex_token
|
|
62
|
+
return nil unless credential_source_probing_enabled?
|
|
63
|
+
|
|
58
64
|
data = read_json(CODEX_AUTH)
|
|
59
65
|
mode = data[:auth_mode] || data['auth_mode']
|
|
60
66
|
return nil unless mode == 'chatgpt'
|
|
@@ -66,8 +72,9 @@ module Legion
|
|
|
66
72
|
token
|
|
67
73
|
end
|
|
68
74
|
|
|
69
|
-
# Read the OPENAI_API_KEY from ~/.codex/auth.json.
|
|
70
75
|
def codex_openai_key
|
|
76
|
+
return nil unless credential_source_probing_enabled?
|
|
77
|
+
|
|
71
78
|
data = read_json(CODEX_AUTH)
|
|
72
79
|
val = data[:OPENAI_API_KEY] || data['OPENAI_API_KEY']
|
|
73
80
|
return nil if val.nil?
|
|
@@ -206,7 +213,8 @@ module Legion
|
|
|
206
213
|
false
|
|
207
214
|
end
|
|
208
215
|
|
|
209
|
-
module_function :env, :
|
|
216
|
+
module_function :env, :credential_source_probing_enabled?,
|
|
217
|
+
:claude_config, :claude_config_value,
|
|
210
218
|
:claude_env_value, :codex_token, :codex_openai_key,
|
|
211
219
|
:setting, :socket_open?, :http_ok?,
|
|
212
220
|
:dedup_credentials, :credential_hash,
|
|
@@ -28,7 +28,7 @@ module Legion
|
|
|
28
28
|
verification_key: signing_key,
|
|
29
29
|
issuer: issuer,
|
|
30
30
|
algorithm: algorithm,
|
|
31
|
-
verify_issuer:
|
|
31
|
+
verify_issuer: true
|
|
32
32
|
))
|
|
33
33
|
validate_registered_claims!(claims)
|
|
34
34
|
validate_request_expiry!(claims)
|
|
@@ -72,15 +72,30 @@ module Legion
|
|
|
72
72
|
raise TokenError, 'fleet request expires_at is invalid'
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
+
SCALAR_CLAIMS = %i[
|
|
76
|
+
request_id correlation_id idempotency_key operation provider provider_instance
|
|
77
|
+
model reply_to timeout_seconds expires_at
|
|
78
|
+
].freeze
|
|
79
|
+
HASHABLE_CLAIMS = %i[message_context params caller trace_context].freeze
|
|
80
|
+
|
|
75
81
|
def validate_envelope_claims!(claims, envelope)
|
|
76
|
-
|
|
77
|
-
request_id correlation_id idempotency_key operation provider provider_instance
|
|
78
|
-
model reply_to message_context params caller trace_context timeout_seconds expires_at
|
|
79
|
-
].each do |key|
|
|
82
|
+
SCALAR_CLAIMS.each do |key|
|
|
80
83
|
expected = canonical_value(envelope[key])
|
|
81
84
|
actual = canonical_value(claims[key])
|
|
82
85
|
raise TokenError, "fleet token #{key} claim mismatch" unless actual == expected
|
|
83
86
|
end
|
|
87
|
+
|
|
88
|
+
HASHABLE_CLAIMS.each do |key|
|
|
89
|
+
hash_key = :"#{key}_hash"
|
|
90
|
+
expected_hash = content_hash(envelope[key])
|
|
91
|
+
actual_hash = claims[hash_key] || content_hash(claims[key])
|
|
92
|
+
raise TokenError, "fleet token #{key} hash mismatch" unless actual_hash == expected_hash
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def content_hash(value)
|
|
97
|
+
require 'digest'
|
|
98
|
+
Digest::SHA256.hexdigest(canonical_value(value).to_s)
|
|
84
99
|
end
|
|
85
100
|
|
|
86
101
|
def reserve_replay!(jti)
|
|
@@ -119,8 +134,16 @@ module Legion
|
|
|
119
134
|
@replay_mutex.synchronize { purge_replay_cache_locked!(Time.now.to_i) }
|
|
120
135
|
end
|
|
121
136
|
|
|
137
|
+
MAX_REPLAY_ENTRIES = 100_000
|
|
138
|
+
|
|
122
139
|
def purge_replay_cache_locked!(now)
|
|
123
140
|
@seen_jtis.each_pair { |jti, entry| @seen_jtis.delete(jti) unless active_replay?(entry, now) }
|
|
141
|
+
evict_oldest_replay_entries! if @seen_jtis.size > MAX_REPLAY_ENTRIES
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def evict_oldest_replay_entries!
|
|
145
|
+
sorted = @seen_jtis.each_pair.sort_by { |_jti, entry| entry[:expires_at] }
|
|
146
|
+
sorted.first(@seen_jtis.size - MAX_REPLAY_ENTRIES).each_key { |jti| @seen_jtis.delete(jti) }
|
|
124
147
|
end
|
|
125
148
|
|
|
126
149
|
def active_replay?(entry, now)
|
|
@@ -49,10 +49,11 @@ module Legion
|
|
|
49
49
|
TokenValidator.validate!(token: envelope_value(envelope, :signed_token), envelope: envelope)
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
-
def validate_policy!(_envelope)
|
|
52
|
+
def validate_policy!(_envelope) # rubocop:disable Naming/PredicateMethod
|
|
53
53
|
return true unless responder_setting(:require_policy, default: false)
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
log.warn('[fleet] require_policy is enabled but no policy engine is configured — allowing request')
|
|
56
|
+
true
|
|
56
57
|
end
|
|
57
58
|
|
|
58
59
|
def validate_idempotency!(envelope)
|
|
@@ -115,12 +116,22 @@ module Legion
|
|
|
115
116
|
TokenValidator.release_replay!(claims[:jti])
|
|
116
117
|
end
|
|
117
118
|
|
|
119
|
+
MAX_IDEMPOTENCY_ENTRIES = 100_000
|
|
120
|
+
|
|
118
121
|
def purge_idempotency_cache!
|
|
119
122
|
@idempotency_mutex.synchronize do
|
|
120
123
|
now = Time.now.to_i
|
|
121
124
|
@idempotency_keys.each_pair do |key, entry|
|
|
122
125
|
@idempotency_keys.delete(key) if entry[:expires_at] <= now
|
|
123
126
|
end
|
|
127
|
+
evict_oldest_idempotency_entries! if @idempotency_keys.size > MAX_IDEMPOTENCY_ENTRIES
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def evict_oldest_idempotency_entries!
|
|
132
|
+
sorted = @idempotency_keys.each_pair.sort_by { |_key, entry| entry[:expires_at] }
|
|
133
|
+
sorted.first(@idempotency_keys.size - MAX_IDEMPOTENCY_ENTRIES).each_key do |key|
|
|
134
|
+
@idempotency_keys.delete(key)
|
|
124
135
|
end
|
|
125
136
|
end
|
|
126
137
|
|
|
@@ -56,6 +56,12 @@ module Legion
|
|
|
56
56
|
def tools? = capabilities.include?(:tools)
|
|
57
57
|
def thinking? = capabilities.include?(:thinking)
|
|
58
58
|
|
|
59
|
+
# Returns true if the model supports prompt caching (Anthropic Claude 4.x, 3.5 Sonnet+).
|
|
60
|
+
# Checks the explicit `prompt_caching` capability flag in the capabilities array.
|
|
61
|
+
def supports_prompt_caching?
|
|
62
|
+
capabilities.include?(:prompt_caching)
|
|
63
|
+
end
|
|
64
|
+
|
|
59
65
|
def supports?(capability)
|
|
60
66
|
capabilities.include?(capability.to_s.downcase.to_sym)
|
|
61
67
|
end
|
|
@@ -122,7 +128,7 @@ module Legion
|
|
|
122
128
|
end
|
|
123
129
|
|
|
124
130
|
# Legacy capability predicates (string-based)
|
|
125
|
-
%w[function_calling structured_output batch reasoning citations streaming].each do |cap|
|
|
131
|
+
%w[function_calling structured_output batch reasoning citations streaming prompt_caching].each do |cap|
|
|
126
132
|
define_method "#{cap}?" do
|
|
127
133
|
supports?(cap)
|
|
128
134
|
end
|