lex-llm 0.4.18 → 0.5.0
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 +13 -2
- data/B1b-conformance-kit.md +79 -0
- data/CHANGELOG.md +19 -0
- data/lex-llm.gemspec +2 -3
- data/lib/legion/extensions/llm/attachment.rb +1 -1
- data/lib/legion/extensions/llm/canonical/chunk.rb +184 -0
- data/lib/legion/extensions/llm/canonical/content_block.rb +126 -0
- data/lib/legion/extensions/llm/canonical/message.rb +125 -0
- data/lib/legion/extensions/llm/canonical/params.rb +61 -0
- data/lib/legion/extensions/llm/canonical/request.rb +117 -0
- data/lib/legion/extensions/llm/canonical/response.rb +124 -0
- data/lib/legion/extensions/llm/canonical/thinking.rb +81 -0
- data/lib/legion/extensions/llm/canonical/tool_call.rb +134 -0
- data/lib/legion/extensions/llm/canonical/tool_definition.rb +73 -0
- data/lib/legion/extensions/llm/canonical/usage.rb +61 -0
- data/lib/legion/extensions/llm/canonical.rb +49 -0
- data/lib/legion/extensions/llm/chat.rb +3 -5
- data/lib/legion/extensions/llm/connection.rb +5 -1
- data/lib/legion/extensions/llm/error.rb +3 -7
- data/lib/legion/extensions/llm/fleet/envelope_validation.rb +1 -3
- data/lib/legion/extensions/llm/fleet/provider_responder.rb +1 -3
- data/lib/legion/extensions/llm/fleet/token_validator.rb +1 -3
- data/lib/legion/extensions/llm/model/info.rb +4 -6
- data/lib/legion/extensions/llm/models.rb +3 -3
- data/lib/legion/extensions/llm/provider/open_ai_compatible.rb +7 -3
- data/lib/legion/extensions/llm/routing/lane_key.rb +1 -3
- data/lib/legion/extensions/llm/stream_accumulator.rb +1 -1
- data/lib/legion/extensions/llm/streaming.rb +1 -3
- data/lib/legion/extensions/llm/tool.rb +1 -3
- data/lib/legion/extensions/llm/version.rb +1 -1
- data/lib/legion/extensions/llm.rb +118 -35
- data/spec/fixtures/ruby.mp3 +0 -0
- data/spec/fixtures/ruby.mp4 +0 -0
- data/spec/fixtures/ruby.png +0 -0
- data/spec/fixtures/ruby.txt +1 -0
- data/spec/fixtures/ruby.wav +0 -0
- data/spec/fixtures/ruby.xml +1 -0
- data/spec/fixtures/sample.pdf +0 -0
- data/spec/legion/extensions/llm/agent_spec.rb +179 -0
- data/spec/legion/extensions/llm/attachment_spec.rb +25 -0
- data/spec/legion/extensions/llm/auto_registration_spec.rb +38 -0
- data/spec/legion/extensions/llm/canonical/chunk_spec.rb +285 -0
- data/spec/legion/extensions/llm/canonical/content_block_spec.rb +179 -0
- data/spec/legion/extensions/llm/canonical/message_spec.rb +203 -0
- data/spec/legion/extensions/llm/canonical/params_spec.rb +159 -0
- data/spec/legion/extensions/llm/canonical/request_spec.rb +174 -0
- data/spec/legion/extensions/llm/canonical/response_spec.rb +234 -0
- data/spec/legion/extensions/llm/canonical/thinking_spec.rb +151 -0
- data/spec/legion/extensions/llm/canonical/tool_call_spec.rb +191 -0
- data/spec/legion/extensions/llm/canonical/tool_definition_spec.rb +174 -0
- data/spec/legion/extensions/llm/canonical/usage_spec.rb +138 -0
- data/spec/legion/extensions/llm/configuration_spec.rb +38 -0
- data/spec/legion/extensions/llm/conformance/client_translator_examples.rb +269 -0
- data/spec/legion/extensions/llm/conformance/conformance.rb +51 -0
- data/spec/legion/extensions/llm/conformance/echo_translator.rb +56 -0
- data/spec/legion/extensions/llm/conformance/echo_translator_spec.rb +13 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_empty_response.json +13 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_error_response.json +19 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_fleet_round_trip.json +81 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_metering_audit_events.json +101 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_params_mapping_request.json +21 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_simple_text_request.json +13 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_simple_text_response.json +13 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_stop_reason_matrix.json +36 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_streaming_accumulated_response.json +20 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_streaming_error_chunks.json +26 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_streaming_text_chunks.json +33 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_streaming_thinking_chunks.json +42 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_streaming_tool_call_chunks.json +41 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_system_prompt_request.json +14 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_thinking_request.json +18 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_thinking_response.json +17 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_tool_results_continuation_request.json +75 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_tool_use_response.json +25 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_tools_request.json +34 -0
- data/spec/legion/extensions/llm/conformance/provider_translator_examples.rb +390 -0
- data/spec/legion/extensions/llm/connection_logging_spec.rb +53 -0
- data/spec/legion/extensions/llm/connection_retry_spec.rb +36 -0
- data/spec/legion/extensions/llm/context_spec.rb +127 -0
- data/spec/legion/extensions/llm/credential_sources_spec.rb +468 -0
- data/spec/legion/extensions/llm/error_middleware_spec.rb +102 -0
- data/spec/legion/extensions/llm/error_spec.rb +87 -0
- data/spec/legion/extensions/llm/fleet/provider_responder_spec.rb +120 -0
- data/spec/legion/extensions/llm/fleet/token_validator_spec.rb +163 -0
- data/spec/legion/extensions/llm/fleet/worker_execution_spec.rb +128 -0
- data/spec/legion/extensions/llm/fleet_messages_spec.rb +402 -0
- data/spec/legion/extensions/llm/gemspec_spec.rb +25 -0
- data/spec/legion/extensions/llm/message_spec.rb +64 -0
- data/spec/legion/extensions/llm/model/info_spec.rb +222 -0
- data/spec/legion/extensions/llm/models_spec.rb +104 -0
- data/spec/legion/extensions/llm/provider/open_ai_compatible_spec.rb +203 -0
- data/spec/legion/extensions/llm/provider_contract_spec.rb +60 -0
- data/spec/legion/extensions/llm/provider_settings_spec.rb +76 -0
- data/spec/legion/extensions/llm/provider_spec.rb +592 -0
- data/spec/legion/extensions/llm/registry_event_builder_spec.rb +68 -0
- data/spec/legion/extensions/llm/registry_publisher_spec.rb +22 -0
- data/spec/legion/extensions/llm/responses/response_objects_spec.rb +75 -0
- data/spec/legion/extensions/llm/responses/thinking_extractor_spec.rb +75 -0
- data/spec/legion/extensions/llm/routing/model_offering_spec.rb +222 -0
- data/spec/legion/extensions/llm/routing/offering_registry_spec.rb +50 -0
- data/spec/legion/extensions/llm/routing/registry_event_spec.rb +120 -0
- data/spec/legion/extensions/llm/stream_accumulator_spec.rb +103 -0
- data/spec/legion/extensions/llm/streaming_spec.rb +108 -0
- data/spec/legion/extensions/llm/tool_spec.rb +94 -0
- data/spec/legion/extensions/llm/transport/fleet_lane_spec.rb +60 -0
- data/spec/legion/extensions/llm/utils_spec.rb +113 -0
- data/spec/legion/extensions/llm_base_contract_spec.rb +110 -0
- data/spec/legion/extensions/llm_extension_spec.rb +78 -0
- data/spec/legion/extensions/llm_root_spec.rb +51 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/support/fake_llm_provider.rb +148 -0
- data/spec/support/llm_configuration.rb +21 -0
- data/spec/support/rspec_configuration.rb +19 -0
- data/spec/support/simplecov_configuration.rb +20 -0
- metadata +96 -15
|
@@ -80,7 +80,7 @@ module Legion
|
|
|
80
80
|
/reduce the length of messages/i
|
|
81
81
|
].freeze
|
|
82
82
|
|
|
83
|
-
def parse_error(provider:, response:)
|
|
83
|
+
def parse_error(provider:, response:)
|
|
84
84
|
response = response_with_stream_error_body(response)
|
|
85
85
|
message = provider&.parse_error(response)
|
|
86
86
|
|
|
@@ -88,9 +88,7 @@ module Legion
|
|
|
88
88
|
when 200..399
|
|
89
89
|
message
|
|
90
90
|
when 400
|
|
91
|
-
if context_length_exceeded?(message)
|
|
92
|
-
raise ContextLengthExceededError.new(response, message || 'Context length exceeded')
|
|
93
|
-
end
|
|
91
|
+
raise ContextLengthExceededError.new(response, message || 'Context length exceeded') if context_length_exceeded?(message)
|
|
94
92
|
|
|
95
93
|
raise BadRequestError.new(response, message || 'Invalid request - please check your input')
|
|
96
94
|
when 401
|
|
@@ -101,9 +99,7 @@ module Legion
|
|
|
101
99
|
raise ForbiddenError.new(response,
|
|
102
100
|
message || 'Forbidden - you do not have permission to access this resource')
|
|
103
101
|
when 429
|
|
104
|
-
if context_length_exceeded?(message)
|
|
105
|
-
raise ContextLengthExceededError.new(response, message || 'Context length exceeded')
|
|
106
|
-
end
|
|
102
|
+
raise ContextLengthExceededError.new(response, message || 'Context length exceeded') if context_length_exceeded?(message)
|
|
107
103
|
|
|
108
104
|
raise RateLimitError.new(response, message || 'Rate limit exceeded - please wait a moment')
|
|
109
105
|
when 500
|
|
@@ -14,9 +14,7 @@ module Legion
|
|
|
14
14
|
|
|
15
15
|
def reject_legacy_options!
|
|
16
16
|
LEGACY_OPTIONS.each do |key|
|
|
17
|
-
if @options.key?(key) || @options.key?(key.to_s)
|
|
18
|
-
raise ArgumentError, "#{key} is not supported by fleet protocol v2"
|
|
19
|
-
end
|
|
17
|
+
raise ArgumentError, "#{key} is not supported by fleet protocol v2" if @options.key?(key) || @options.key?(key.to_s)
|
|
20
18
|
end
|
|
21
19
|
end
|
|
22
20
|
|
|
@@ -119,9 +119,7 @@ module Legion
|
|
|
119
119
|
raise ConfigurationError,
|
|
120
120
|
"fleet provider instance is not configured: #{instance_id}"
|
|
121
121
|
end
|
|
122
|
-
unless truthy?(dig(instance_settings, :fleet, :respond_to_requests))
|
|
123
|
-
raise ConfigurationError, "fleet responses are disabled for provider instance: #{instance_id}"
|
|
124
|
-
end
|
|
122
|
+
raise ConfigurationError, "fleet responses are disabled for provider instance: #{instance_id}" unless truthy?(dig(instance_settings, :fleet, :respond_to_requests))
|
|
125
123
|
|
|
126
124
|
provider_class.new(deep_symbolize(instance_settings))
|
|
127
125
|
end
|
|
@@ -186,9 +186,7 @@ module Legion
|
|
|
186
186
|
end
|
|
187
187
|
|
|
188
188
|
def signing_key
|
|
189
|
-
if defined?(::Legion::Crypt) && ::Legion::Crypt.respond_to?(:cluster_secret)
|
|
190
|
-
return ::Legion::Crypt.cluster_secret
|
|
191
|
-
end
|
|
189
|
+
return ::Legion::Crypt.cluster_secret if defined?(::Legion::Crypt) && ::Legion::Crypt.respond_to?(:cluster_secret)
|
|
192
190
|
|
|
193
191
|
raise TokenError, 'no signing key available - Legion::Crypt not initialized'
|
|
194
192
|
rescue TokenError
|
|
@@ -19,7 +19,7 @@ module Legion
|
|
|
19
19
|
:parameter_size, :quantization, :size_bytes,
|
|
20
20
|
:modalities_input, :modalities_output, :metadata
|
|
21
21
|
) do
|
|
22
|
-
# rubocop:disable Metrics/ParameterLists
|
|
22
|
+
# rubocop:disable Metrics/ParameterLists
|
|
23
23
|
def initialize(
|
|
24
24
|
id:, name: nil, provider: nil, instance: :default,
|
|
25
25
|
family: nil, capabilities: [], context_length: nil,
|
|
@@ -46,7 +46,7 @@ module Legion
|
|
|
46
46
|
metadata: metadata.is_a?(Hash) ? metadata : {}
|
|
47
47
|
)
|
|
48
48
|
end
|
|
49
|
-
# rubocop:enable Metrics/ParameterLists
|
|
49
|
+
# rubocop:enable Metrics/ParameterLists
|
|
50
50
|
|
|
51
51
|
# ── Capability predicates ─────────────────────────────────────
|
|
52
52
|
|
|
@@ -206,11 +206,9 @@ module Legion
|
|
|
206
206
|
class << self
|
|
207
207
|
private
|
|
208
208
|
|
|
209
|
-
def extract_modalities(data)
|
|
209
|
+
def extract_modalities(data)
|
|
210
210
|
# New-style keys take priority (round-trip from to_h)
|
|
211
|
-
if data.key?(:modalities_input) || data.key?(:modalities_output)
|
|
212
|
-
return [Array(data[:modalities_input]), Array(data[:modalities_output])]
|
|
213
|
-
end
|
|
211
|
+
return [Array(data[:modalities_input]), Array(data[:modalities_output])] if data.key?(:modalities_input) || data.key?(:modalities_output)
|
|
214
212
|
|
|
215
213
|
# Legacy: modalities is a hash or Modalities object
|
|
216
214
|
modalities_data = data[:modalities]
|
|
@@ -123,7 +123,7 @@ module Legion
|
|
|
123
123
|
fetch_provider_models(remote_only: remote_only)[:models]
|
|
124
124
|
end
|
|
125
125
|
|
|
126
|
-
def resolve(model_id, provider: nil, assume_exists: false, config: nil)
|
|
126
|
+
def resolve(model_id, provider: nil, assume_exists: false, config: nil)
|
|
127
127
|
config ||= Legion::Extensions::Llm.config
|
|
128
128
|
provider_class = provider ? resolve_provider_class(provider) : nil
|
|
129
129
|
|
|
@@ -168,7 +168,7 @@ module Legion
|
|
|
168
168
|
instance.respond_to?(method, include_private) || super
|
|
169
169
|
end
|
|
170
170
|
|
|
171
|
-
def fetch_models_dev_models(existing_models)
|
|
171
|
+
def fetch_models_dev_models(existing_models)
|
|
172
172
|
log.info 'Fetching models from models.dev API...'
|
|
173
173
|
|
|
174
174
|
connection = Connection.basic do |f|
|
|
@@ -300,7 +300,7 @@ module Legion
|
|
|
300
300
|
end
|
|
301
301
|
end
|
|
302
302
|
|
|
303
|
-
def add_provider_metadata(models_dev_model, provider_model)
|
|
303
|
+
def add_provider_metadata(models_dev_model, provider_model)
|
|
304
304
|
data = models_dev_model.to_h
|
|
305
305
|
data[:name] = provider_model.name if blank_value?(data[:name])
|
|
306
306
|
data[:family] = provider_model.family if blank_value?(data[:family])
|
|
@@ -92,12 +92,16 @@ module Legion
|
|
|
92
92
|
return nil if tools.empty?
|
|
93
93
|
|
|
94
94
|
tools.values.map do |tool|
|
|
95
|
+
# Tools can be ToolDefinition objects or plain Hashes from native_dispatch.
|
|
96
|
+
tool_name = tool.respond_to?(:name) ? tool.name : (tool[:name] || tool['name'])
|
|
97
|
+
tool_desc = tool.respond_to?(:description) ? tool.description : (tool[:description] || tool['description'] || '')
|
|
98
|
+
tool_params = tool.respond_to?(:params_schema) ? tool.params_schema : (tool[:parameters] || tool['parameters'] || {})
|
|
95
99
|
{
|
|
96
100
|
type: 'function',
|
|
97
101
|
function: {
|
|
98
|
-
name:
|
|
99
|
-
description:
|
|
100
|
-
parameters:
|
|
102
|
+
name: tool_name,
|
|
103
|
+
description: tool_desc,
|
|
104
|
+
parameters: tool_params || { type: 'object', properties: {} }
|
|
101
105
|
}
|
|
102
106
|
}
|
|
103
107
|
end
|
|
@@ -10,9 +10,7 @@ module Legion
|
|
|
10
10
|
|
|
11
11
|
def for(offering, prefix: 'llm.fleet', include_context: true, include_fingerprint: false)
|
|
12
12
|
parts = [prefix, lane_kind(offering), model_slug(lane_model(offering))]
|
|
13
|
-
if include_context && offering.inference? && offering.context_window
|
|
14
|
-
parts << "ctx#{offering.context_window}"
|
|
15
|
-
end
|
|
13
|
+
parts << "ctx#{offering.context_window}" if include_context && offering.inference? && offering.context_window
|
|
16
14
|
parts.push('elig', eligibility_fingerprint(offering)) if include_fingerprint
|
|
17
15
|
parts.join('.')
|
|
18
16
|
end
|
|
@@ -39,7 +39,7 @@ module Legion
|
|
|
39
39
|
log.debug { inspect } if Legion::Extensions::Llm.config.log_stream_debug
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
-
def filtered_chunk(chunk)
|
|
42
|
+
def filtered_chunk(chunk)
|
|
43
43
|
has_content = !@last_content_delta.empty?
|
|
44
44
|
has_thinking = !@last_thinking_delta.empty?
|
|
45
45
|
has_tokens = chunk.input_tokens&.positive? || chunk.output_tokens&.positive?
|
|
@@ -16,9 +16,7 @@ module Legion
|
|
|
16
16
|
response = connection.post stream_url, payload do |req|
|
|
17
17
|
req.headers = additional_headers.merge(req.headers) unless additional_headers.empty?
|
|
18
18
|
on_chunk = build_stream_callback(accumulator, block)
|
|
19
|
-
if Legion::Extensions::Llm.config.log_stream_debug
|
|
20
|
-
log.debug { "Stream callback prepared: #{on_chunk.inspect}" }
|
|
21
|
-
end
|
|
19
|
+
log.debug { "Stream callback prepared: #{on_chunk.inspect}" } if Legion::Extensions::Llm.config.log_stream_debug
|
|
22
20
|
if faraday_1?
|
|
23
21
|
req.options[:on_data] = handle_stream(&on_chunk)
|
|
24
22
|
else
|
|
@@ -235,9 +235,7 @@ module Legion
|
|
|
235
235
|
def resolve_direct_schema(schema)
|
|
236
236
|
return extract_schema(schema.to_json_schema) if schema.respond_to?(:to_json_schema)
|
|
237
237
|
return Legion::Extensions::Llm::Utils.deep_dup(schema) if schema.is_a?(Hash)
|
|
238
|
-
if schema.is_a?(Class) && schema.method_defined?(:to_json_schema)
|
|
239
|
-
return extract_schema(schema.new.to_json_schema)
|
|
240
|
-
end
|
|
238
|
+
return extract_schema(schema.new.to_json_schema) if schema.is_a?(Class) && schema.method_defined?(:to_json_schema)
|
|
241
239
|
|
|
242
240
|
nil
|
|
243
241
|
end
|
|
@@ -23,29 +23,125 @@ require 'marcel'
|
|
|
23
23
|
require 'ruby_llm/schema'
|
|
24
24
|
require 'securerandom'
|
|
25
25
|
require 'time'
|
|
26
|
-
require 'zeitwerk'
|
|
27
26
|
require_relative 'llm/version'
|
|
28
27
|
|
|
29
28
|
module Legion
|
|
30
29
|
module Extensions
|
|
31
30
|
# Legion-native namespace for the shared LLM provider framework.
|
|
32
31
|
module Llm
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
32
|
+
# ------------------------------------------------------------------ #
|
|
33
|
+
# Explicit requires (replaces Zeitwerk autoloading). #
|
|
34
|
+
# Load order: base classes & canonical types first, then anything #
|
|
35
|
+
# that references them. All live under Legion::Extensions::Llm so #
|
|
36
|
+
# unqualified constant lookups resolve via Ruby scope. #
|
|
37
|
+
# ------------------------------------------------------------------ #
|
|
38
|
+
|
|
39
|
+
# --- Base value objects (no internal deps) ---
|
|
40
|
+
require_relative 'llm/mime_type'
|
|
41
|
+
require_relative 'llm/model/info'
|
|
42
|
+
require_relative 'llm/model/modalities'
|
|
43
|
+
require_relative 'llm/model/pricing_category'
|
|
44
|
+
require_relative 'llm/model/pricing_tier'
|
|
45
|
+
require_relative 'llm/model/pricing'
|
|
46
|
+
require_relative 'llm/configuration'
|
|
47
|
+
require_relative 'llm/thinking'
|
|
48
|
+
require_relative 'llm/tokens'
|
|
49
|
+
require_relative 'llm/message'
|
|
50
|
+
require_relative 'llm/tool_call'
|
|
51
|
+
require_relative 'llm/content'
|
|
52
|
+
require_relative 'llm/errors/unsupported_capability'
|
|
53
|
+
require_relative 'llm/error'
|
|
54
|
+
|
|
55
|
+
# --- Build on message/base types ---
|
|
56
|
+
require_relative 'llm/chunk'
|
|
57
|
+
require_relative 'llm/model'
|
|
58
|
+
require_relative 'llm/attachment'
|
|
59
|
+
|
|
60
|
+
# --- Streaming fundamentals (must load before streaming/provider) ---
|
|
61
|
+
require_relative 'llm/stream_accumulator'
|
|
62
|
+
require_relative 'llm/responses/stream_chunk'
|
|
63
|
+
require_relative 'llm/streaming'
|
|
64
|
+
|
|
65
|
+
# --- Context, Connection ---
|
|
66
|
+
require_relative 'llm/context'
|
|
67
|
+
require_relative 'llm/connection'
|
|
68
|
+
|
|
69
|
+
# --- Response normalizers ---
|
|
70
|
+
require_relative 'llm/responses/chat_response'
|
|
71
|
+
require_relative 'llm/responses/embedding_response'
|
|
72
|
+
require_relative 'llm/responses/thinking_extractor'
|
|
73
|
+
|
|
74
|
+
# --- Provider base & allied modules ---
|
|
75
|
+
require_relative 'llm/provider_contract'
|
|
76
|
+
require_relative 'llm/provider_settings'
|
|
77
|
+
require_relative 'llm/provider'
|
|
78
|
+
|
|
79
|
+
# --- Provider subtypes ---
|
|
80
|
+
require_relative 'llm/provider/open_ai_compatible'
|
|
81
|
+
|
|
82
|
+
# --- Routing ---
|
|
83
|
+
require_relative 'llm/routing'
|
|
84
|
+
require_relative 'llm/routing/lane_key'
|
|
85
|
+
require_relative 'llm/routing/offering_registry'
|
|
86
|
+
require_relative 'llm/routing/registry_event'
|
|
87
|
+
require_relative 'llm/routing/model_offering'
|
|
88
|
+
|
|
89
|
+
# --- Models (scans for Provider subclasses) ---
|
|
90
|
+
require_relative 'llm/models'
|
|
91
|
+
|
|
92
|
+
# --- Agent & Chat (reference Provider, Context, Chat at method-time) ---
|
|
93
|
+
require_relative 'llm/agent'
|
|
94
|
+
require_relative 'llm/chat'
|
|
95
|
+
|
|
96
|
+
# --- Domain services ---
|
|
97
|
+
require_relative 'llm/embedding'
|
|
98
|
+
require_relative 'llm/moderation'
|
|
99
|
+
require_relative 'llm/image'
|
|
100
|
+
require_relative 'llm/transcription'
|
|
101
|
+
|
|
102
|
+
# --- Registry & misc support ---
|
|
103
|
+
require_relative 'llm/registry_event_builder'
|
|
104
|
+
require_relative 'llm/registry_publisher'
|
|
105
|
+
require_relative 'llm/auto_registration'
|
|
106
|
+
require_relative 'llm/credential_sources'
|
|
107
|
+
require_relative 'llm/tool'
|
|
108
|
+
require_relative 'llm/utils'
|
|
109
|
+
require_relative 'llm/aliases'
|
|
110
|
+
|
|
111
|
+
# --- Fleet protocol (depends on Provider, Models) ---
|
|
112
|
+
require_relative 'llm/fleet/protocol'
|
|
113
|
+
require_relative 'llm/fleet/settings'
|
|
114
|
+
require_relative 'llm/fleet/token_error'
|
|
115
|
+
require_relative 'llm/fleet/envelope_validation'
|
|
116
|
+
require_relative 'llm/fleet/publish_safety'
|
|
117
|
+
require_relative 'llm/fleet/default_exchange_reply'
|
|
118
|
+
require_relative 'llm/fleet/token_validator'
|
|
119
|
+
require_relative 'llm/fleet/worker_execution'
|
|
120
|
+
require_relative 'llm/fleet/provider_responder'
|
|
121
|
+
|
|
122
|
+
# --- Transport lane (references Fleet exchange/message autoloads) ---
|
|
123
|
+
require_relative 'llm/transport/fleet_lane'
|
|
124
|
+
|
|
125
|
+
# --- Canonical types — explicit self-contained loader ---
|
|
126
|
+
require_relative 'llm/canonical'
|
|
127
|
+
|
|
128
|
+
# --- Transport modules (lazy — depend on optional legion-transport) ---
|
|
129
|
+
# These remain as autoload so boot-time does not force legion-transport.
|
|
130
|
+
module Transport
|
|
131
|
+
# Shared AMQP exchange definitions for fleet routing.
|
|
132
|
+
# Lazy-loaded; only instantiated when legion-transport is available.
|
|
133
|
+
module Exchanges
|
|
134
|
+
autoload :Fleet, File.expand_path('llm/transport/exchanges/fleet', __dir__)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Shared AMQP message envelopes for fleet request/response cycles.
|
|
138
|
+
# Lazy-loaded; only instantiated when legion-transport is available.
|
|
139
|
+
module Messages
|
|
140
|
+
autoload :FleetRequest, File.expand_path('llm/transport/messages/fleet_request', __dir__)
|
|
141
|
+
autoload :FleetResponse, File.expand_path('llm/transport/messages/fleet_response', __dir__)
|
|
142
|
+
autoload :FleetError, File.expand_path('llm/transport/messages/fleet_error', __dir__)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
49
145
|
|
|
50
146
|
Schema = ::RubyLLM::Schema unless const_defined?(:Schema, false)
|
|
51
147
|
|
|
@@ -148,6 +244,11 @@ module Legion
|
|
|
148
244
|
require_policy: false,
|
|
149
245
|
require_idempotency: true,
|
|
150
246
|
idempotency_ttl_seconds: 600
|
|
247
|
+
},
|
|
248
|
+
request: {
|
|
249
|
+
logger: {
|
|
250
|
+
request_payload: false
|
|
251
|
+
}
|
|
151
252
|
}
|
|
152
253
|
}
|
|
153
254
|
}
|
|
@@ -156,24 +257,6 @@ module Legion
|
|
|
156
257
|
def self.provider_settings(...)
|
|
157
258
|
ProviderSettings.build(...)
|
|
158
259
|
end
|
|
159
|
-
|
|
160
|
-
require_relative 'llm/auto_registration'
|
|
161
|
-
require_relative 'llm/credential_sources'
|
|
162
|
-
loader.eager_load
|
|
163
|
-
|
|
164
|
-
module Transport
|
|
165
|
-
# Local autoloads for fleet exchange classes that depend on legion-transport.
|
|
166
|
-
module Exchanges
|
|
167
|
-
autoload :Fleet, File.expand_path('llm/transport/exchanges/fleet', __dir__)
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
# Local autoloads for fleet message classes that depend on legion-transport.
|
|
171
|
-
module Messages
|
|
172
|
-
autoload :FleetRequest, File.expand_path('llm/transport/messages/fleet_request', __dir__)
|
|
173
|
-
autoload :FleetResponse, File.expand_path('llm/transport/messages/fleet_response', __dir__)
|
|
174
|
-
autoload :FleetError, File.expand_path('llm/transport/messages/fleet_error', __dir__)
|
|
175
|
-
end
|
|
176
|
-
end
|
|
177
260
|
end
|
|
178
261
|
end
|
|
179
262
|
end
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Ruby is the best.
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<truism>Ruby is the best</truism>
|
|
Binary file
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::Llm::Agent do
|
|
6
|
+
include_context 'with configured Legion::Extensions::Llm'
|
|
7
|
+
include_context 'with fake llm provider'
|
|
8
|
+
|
|
9
|
+
it 'builds a configured plain chat via .chat with runtime inputs' do
|
|
10
|
+
tool_class = Class.new(Legion::Extensions::Llm::Tool) do
|
|
11
|
+
def name = 'echo_tool'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
agent_class = Class.new(Legion::Extensions::Llm::Agent) do
|
|
15
|
+
model 'fake-chat-model', provider: :fake_llm, assume_model_exists: true
|
|
16
|
+
inputs :display_name
|
|
17
|
+
instructions { "Hello #{display_name}" }
|
|
18
|
+
tools { [tool_class.new] }
|
|
19
|
+
params { { max_tokens: 12 } }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
chat = agent_class.chat(display_name: 'Ava')
|
|
23
|
+
|
|
24
|
+
expect(chat.messages.first.role).to eq(:system)
|
|
25
|
+
expect(chat.messages.first.content).to eq('Hello Ava')
|
|
26
|
+
expect(chat.tools.keys).to include(:echo_tool)
|
|
27
|
+
expect(chat.params).to eq(max_tokens: 12)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'exposes Legion::Extensions::Llm::Chat as chat in execution context for .chat' do
|
|
31
|
+
agent_class = Class.new(Legion::Extensions::Llm::Agent) do
|
|
32
|
+
model 'fake-chat-model', provider: :fake_llm, assume_model_exists: true
|
|
33
|
+
instructions { chat.class.name }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
chat = agent_class.chat
|
|
37
|
+
expect(chat.messages.first.content).to eq('Legion::Extensions::Llm::Chat')
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'raises when instructions default prompt is missing' do
|
|
41
|
+
agent_class = Class.new(Legion::Extensions::Llm::Agent) do
|
|
42
|
+
model 'fake-chat-model', provider: :fake_llm, assume_model_exists: true
|
|
43
|
+
instructions
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
expect { agent_class.chat }.to raise_error(Legion::Extensions::Llm::PromptNotFoundError, /Prompt file not found/)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'supports inline schema DSL via schema do ... end' do
|
|
50
|
+
agent_class = Class.new(Legion::Extensions::Llm::Agent) do
|
|
51
|
+
model 'fake-chat-model', provider: :fake_llm, assume_model_exists: true
|
|
52
|
+
schema do
|
|
53
|
+
string :verdict, enum: %w[pass revise]
|
|
54
|
+
string :feedback
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
chat = agent_class.chat
|
|
59
|
+
|
|
60
|
+
expect(chat.schema).to include(name: 'Schema', strict: true, schema: include(type: 'object'))
|
|
61
|
+
expect(chat.schema.dig(:schema, :properties)).to include(
|
|
62
|
+
verdict: include(type: 'string'),
|
|
63
|
+
feedback: include(type: 'string')
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it 'supports runtime-evaluated schema blocks that return a schema value' do
|
|
68
|
+
agent_class = Class.new(Legion::Extensions::Llm::Agent) do
|
|
69
|
+
model 'fake-chat-model', provider: :fake_llm, assume_model_exists: true
|
|
70
|
+
inputs :strict
|
|
71
|
+
|
|
72
|
+
schema do
|
|
73
|
+
if strict
|
|
74
|
+
{
|
|
75
|
+
type: 'object',
|
|
76
|
+
properties: { answer: { type: 'string' } },
|
|
77
|
+
required: ['answer'],
|
|
78
|
+
additionalProperties: false
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
strict_chat = agent_class.chat(strict: true)
|
|
85
|
+
loose_chat = agent_class.chat(strict: false)
|
|
86
|
+
|
|
87
|
+
expect(strict_chat.schema).to include(name: 'response', strict: true, schema: include(type: 'object'))
|
|
88
|
+
expect(loose_chat.schema).to be_nil
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it 'can ask using a registered provider' do
|
|
92
|
+
agent_class = Class.new(Legion::Extensions::Llm::Agent) do
|
|
93
|
+
model 'fake-chat-model', provider: :fake_llm, assume_model_exists: true
|
|
94
|
+
instructions 'Answer questions clearly.'
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
stub_const('SpecChatAgent', agent_class)
|
|
98
|
+
|
|
99
|
+
response = SpecChatAgent.new.ask('hello')
|
|
100
|
+
expect(response.content).to include('fake response to hello')
|
|
101
|
+
expect(response.role).to eq(:assistant)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it 'delegates add_message to the underlying chat interface' do
|
|
105
|
+
agent_class = Class.new(Legion::Extensions::Llm::Agent) do
|
|
106
|
+
model 'fake-chat-model', provider: :fake_llm, assume_model_exists: true
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
agent = agent_class.new
|
|
110
|
+
message = agent.add_message(role: :user, content: 'Hello')
|
|
111
|
+
|
|
112
|
+
expect(message.role).to eq(:user)
|
|
113
|
+
expect(message.content).to eq('Hello')
|
|
114
|
+
expect(agent.chat.messages.last).to eq(message)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it 'exposes messages like Legion::Extensions::Llm::Chat' do
|
|
118
|
+
agent_class = Class.new(Legion::Extensions::Llm::Agent) do
|
|
119
|
+
model 'fake-chat-model', provider: :fake_llm, assume_model_exists: true
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
agent = agent_class.new
|
|
123
|
+
agent.add_message(role: :user, content: 'First')
|
|
124
|
+
|
|
125
|
+
expect(agent.messages).to eq(agent.chat.messages)
|
|
126
|
+
expect(agent.messages.last.content).to eq('First')
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it 'delegates callback hooks to the underlying chat' do
|
|
130
|
+
fake_chat = Class.new do
|
|
131
|
+
attr_reader :events
|
|
132
|
+
|
|
133
|
+
def initialize
|
|
134
|
+
@events = []
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def on_new_message(&)
|
|
138
|
+
@events << :new_message
|
|
139
|
+
self
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def on_end_message(&)
|
|
143
|
+
@events << :end_message
|
|
144
|
+
self
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def on_tool_call(&)
|
|
148
|
+
@events << :tool_call
|
|
149
|
+
self
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def on_tool_result(&)
|
|
153
|
+
@events << :tool_result
|
|
154
|
+
self
|
|
155
|
+
end
|
|
156
|
+
end.new
|
|
157
|
+
|
|
158
|
+
agent = Class.new(described_class).new(chat: fake_chat)
|
|
159
|
+
|
|
160
|
+
expect(agent.on_new_message { :ok }).to eq(fake_chat)
|
|
161
|
+
expect(agent.on_end_message { :ok }).to eq(fake_chat)
|
|
162
|
+
expect(agent.on_tool_call { :ok }).to eq(fake_chat)
|
|
163
|
+
expect(agent.on_tool_result { :ok }).to eq(fake_chat)
|
|
164
|
+
expect(fake_chat.events).to eq(%i[new_message end_message tool_call tool_result])
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
it 'supports Enumerable by delegating each to chat' do
|
|
168
|
+
fake_chat = Class.new do
|
|
169
|
+
def each(&)
|
|
170
|
+
return enum_for(:each) unless block_given?
|
|
171
|
+
|
|
172
|
+
%w[first second].each(&)
|
|
173
|
+
end
|
|
174
|
+
end.new
|
|
175
|
+
|
|
176
|
+
agent = Class.new(described_class).new(chat: fake_chat)
|
|
177
|
+
expect(agent.map(&:upcase)).to eq(%w[FIRST SECOND])
|
|
178
|
+
end
|
|
179
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'open3'
|
|
5
|
+
require 'rbconfig'
|
|
6
|
+
|
|
7
|
+
RSpec.describe Legion::Extensions::Llm::Attachment do
|
|
8
|
+
it 'supports path attachments from the public API' do
|
|
9
|
+
script = <<~'RUBY'
|
|
10
|
+
require 'legion/extensions/llm'
|
|
11
|
+
|
|
12
|
+
content = Legion::Extensions::Llm::Content.new('What is in this file?', 'spec/fixtures/ruby.txt')
|
|
13
|
+
attachment = content.attachments.first
|
|
14
|
+
puts "#{attachment.filename},#{attachment.mime_type}"
|
|
15
|
+
RUBY
|
|
16
|
+
|
|
17
|
+
stdout, stderr, status = Open3.capture3(
|
|
18
|
+
RbConfig.ruby, '-Ilib', '-e', script,
|
|
19
|
+
chdir: File.expand_path('../../../..', __dir__)
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
expect(status.success?).to be(true), stderr
|
|
23
|
+
expect(stdout.strip).to eq('ruby.txt,text/plain')
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::Llm::AutoRegistration do
|
|
6
|
+
# Build a fake provider module that extends AutoRegistration,
|
|
7
|
+
# mimicking what a real lex-llm-* provider would look like.
|
|
8
|
+
let(:fake_provider_class) { Class.new }
|
|
9
|
+
|
|
10
|
+
let(:provider_module) do
|
|
11
|
+
klass = fake_provider_class
|
|
12
|
+
mod = Module.new do
|
|
13
|
+
extend Legion::Extensions::Llm::AutoRegistration
|
|
14
|
+
|
|
15
|
+
const_set(:PROVIDER_FAMILY, :fake_provider)
|
|
16
|
+
|
|
17
|
+
define_singleton_method(:provider_class) { klass }
|
|
18
|
+
end
|
|
19
|
+
mod
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
describe '#discover_instances' do
|
|
23
|
+
it 'returns an empty hash by default' do
|
|
24
|
+
expect(provider_module.discover_instances).to eq({})
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
describe '#provider_aliases' do
|
|
29
|
+
it 'returns an empty alias list by default' do
|
|
30
|
+
expect(provider_module.provider_aliases).to eq([])
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'does not expose legion-llm registry mutation hooks' do
|
|
35
|
+
expect(provider_module).not_to respond_to(:register_discovered_instances)
|
|
36
|
+
expect(provider_module).not_to respond_to(:rediscover!)
|
|
37
|
+
end
|
|
38
|
+
end
|