lex-llm 0.4.16 → 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 +33 -0
- data/README.md +349 -153
- data/lex-llm.gemspec +3 -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 +14 -2
- 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 +12 -4
- 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 +6 -4
- 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 +110 -15
|
@@ -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])
|
|
@@ -66,7 +66,11 @@ module Legion
|
|
|
66
66
|
def sanitize_openai_text(text, role:)
|
|
67
67
|
return text unless role.to_sym == :assistant && text.is_a?(String)
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
# Preserve thinking tags in the content — qwen3.6 outputs thinking in
|
|
70
|
+
# <think> tags and expects to see its own reasoning on subsequent rounds.
|
|
71
|
+
# The Anthropic API layer separates thinking into distinct content blocks
|
|
72
|
+
# for client-facing responses; the OpenAI compat layer passes them through.
|
|
73
|
+
text
|
|
70
74
|
end
|
|
71
75
|
|
|
72
76
|
def format_openai_tool_calls(tool_calls)
|
|
@@ -88,12 +92,16 @@ module Legion
|
|
|
88
92
|
return nil if tools.empty?
|
|
89
93
|
|
|
90
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'] || {})
|
|
91
99
|
{
|
|
92
100
|
type: 'function',
|
|
93
101
|
function: {
|
|
94
|
-
name:
|
|
95
|
-
description:
|
|
96
|
-
parameters:
|
|
102
|
+
name: tool_name,
|
|
103
|
+
description: tool_desc,
|
|
104
|
+
parameters: tool_params || { type: 'object', properties: {} }
|
|
97
105
|
}
|
|
98
106
|
}
|
|
99
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
|
|
@@ -212,7 +210,11 @@ module Legion
|
|
|
212
210
|
|
|
213
211
|
def v2_on_data(on_chunk, on_failed_response)
|
|
214
212
|
proc do |chunk, _bytes, env|
|
|
215
|
-
|
|
213
|
+
# Typhoeus/libcurl sends on_data callbacks before headers arrive, so env&.status
|
|
214
|
+
# may be nil or 0 during streaming. Only treat as failure when we have a
|
|
215
|
+
# definitive non-200 status (e.g. 400, 500) and still have data to process.
|
|
216
|
+
status = env&.status
|
|
217
|
+
if status == 200 || status.nil? || status.zero?
|
|
216
218
|
on_chunk.call(chunk, env)
|
|
217
219
|
else
|
|
218
220
|
on_failed_response.call(chunk, env)
|
|
@@ -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
|