lex-llm 0.7.6 → 0.8.4

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.
Files changed (168) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +139 -0
  3. data/RULES.md +97 -0
  4. data/lib/legion/extensions/llm/auto_registration.rb +4 -13
  5. data/lib/legion/extensions/llm/canonical/chunk.rb +107 -94
  6. data/lib/legion/extensions/llm/canonical/content_block.rb +65 -75
  7. data/lib/legion/extensions/llm/canonical/message.rb +53 -69
  8. data/lib/legion/extensions/llm/canonical/params.rb +58 -34
  9. data/lib/legion/extensions/llm/canonical/request.rb +68 -56
  10. data/lib/legion/extensions/llm/canonical/response.rb +62 -78
  11. data/lib/legion/extensions/llm/canonical/strict.rb +105 -0
  12. data/lib/legion/extensions/llm/canonical/thinking.rb +36 -84
  13. data/lib/legion/extensions/llm/canonical/thinking_config.rb +149 -0
  14. data/lib/legion/extensions/llm/canonical/tool_call.rb +43 -53
  15. data/lib/legion/extensions/llm/canonical/tool_definition.rb +56 -46
  16. data/lib/legion/extensions/llm/canonical/tool_schema.rb +15 -22
  17. data/lib/legion/extensions/llm/canonical/usage.rb +47 -40
  18. data/lib/legion/extensions/llm/canonical.rb +7 -5
  19. data/lib/legion/extensions/llm/configuration.rb +40 -10
  20. data/lib/legion/extensions/llm/connection.rb +8 -30
  21. data/lib/legion/extensions/llm/credential_sources.rb +32 -49
  22. data/lib/legion/extensions/llm/discovery/actor.rb +92 -0
  23. data/lib/legion/extensions/llm/discovery/pipeline.rb +604 -0
  24. data/lib/legion/extensions/llm/error.rb +0 -14
  25. data/lib/legion/extensions/llm/fleet/contract_error.rb +15 -0
  26. data/lib/legion/extensions/llm/fleet/envelope_validation.rb +7 -6
  27. data/lib/legion/extensions/llm/fleet/fleet_envelope.rb +66 -0
  28. data/lib/legion/extensions/llm/fleet/protocol.rb +18 -5
  29. data/lib/legion/extensions/llm/fleet/provider_responder.rb +58 -156
  30. data/lib/legion/extensions/llm/fleet/token_validator.rb +15 -21
  31. data/lib/legion/extensions/llm/fleet/worker_execution.rb +107 -154
  32. data/lib/legion/extensions/llm/inventory/errors.rb +0 -1
  33. data/lib/legion/extensions/llm/inventory/evidence.rb +1 -1
  34. data/lib/legion/extensions/llm/inventory/identity.rb +55 -39
  35. data/lib/legion/extensions/llm/inventory/probe_token.rb +9 -12
  36. data/lib/legion/extensions/llm/inventory/publisher.rb +15 -53
  37. data/lib/legion/extensions/llm/inventory/records.rb +36 -100
  38. data/lib/legion/extensions/llm/inventory/registry.rb +54 -48
  39. data/lib/legion/extensions/llm/inventory/snapshot.rb +7 -21
  40. data/lib/legion/extensions/llm/inventory/weight_reconciler.rb +13 -6
  41. data/lib/legion/extensions/llm/inventory/weight_schema.rb +47 -8
  42. data/lib/legion/extensions/llm/provider/open_ai_compatible.rb +178 -182
  43. data/lib/legion/extensions/llm/provider.rb +192 -315
  44. data/lib/legion/extensions/llm/provider_contract.rb +25 -8
  45. data/lib/legion/extensions/llm/provider_settings.rb +5 -26
  46. data/lib/legion/extensions/llm/responses/thinking_extractor.rb +8 -1
  47. data/lib/legion/extensions/llm/responses/tool_arguments.rb +48 -0
  48. data/lib/legion/extensions/llm/routing/provider_outcome.rb +25 -0
  49. data/lib/legion/extensions/llm/routing/records.rb +34 -17
  50. data/lib/legion/extensions/llm/stream_accumulator.rb +186 -270
  51. data/lib/legion/extensions/llm/streaming.rb +50 -35
  52. data/lib/legion/extensions/llm/taxonomies.rb +14 -26
  53. data/lib/legion/extensions/llm/transport/fleet_lane.rb +8 -10
  54. data/lib/legion/extensions/llm/transport/messages/fleet_error.rb +3 -2
  55. data/lib/legion/extensions/llm/transport/messages/fleet_request.rb +6 -8
  56. data/lib/legion/extensions/llm/transport/messages/fleet_response.rb +10 -9
  57. data/lib/legion/extensions/llm/utils.rb +23 -5
  58. data/lib/legion/extensions/llm/version.rb +1 -1
  59. data/lib/legion/extensions/llm.rb +8 -98
  60. data/spec/legion/extensions/llm/auto_registration_spec.rb +4 -9
  61. data/spec/legion/extensions/llm/canonical/chunk_spec.rb +66 -252
  62. data/spec/legion/extensions/llm/canonical/content_block_spec.rb +52 -197
  63. data/spec/legion/extensions/llm/canonical/message_spec.rb +89 -204
  64. data/spec/legion/extensions/llm/canonical/params_spec.rb +55 -136
  65. data/spec/legion/extensions/llm/canonical/request_spec.rb +81 -143
  66. data/spec/legion/extensions/llm/canonical/response_spec.rb +68 -204
  67. data/spec/legion/extensions/llm/canonical/thinking/config_spec.rb +222 -0
  68. data/spec/legion/extensions/llm/canonical/thinking_spec.rb +23 -171
  69. data/spec/legion/extensions/llm/canonical/tool_call_spec.rb +59 -162
  70. data/spec/legion/extensions/llm/canonical/tool_definition_spec.rb +55 -191
  71. data/spec/legion/extensions/llm/canonical/tool_schema_spec.rb +26 -67
  72. data/spec/legion/extensions/llm/canonical/usage_spec.rb +46 -155
  73. data/spec/legion/extensions/llm/configuration_spec.rb +31 -5
  74. data/spec/legion/extensions/llm/conformance/canonical_type_examples.rb +106 -0
  75. data/spec/legion/extensions/llm/conformance/client_translator_examples.rb +1 -2
  76. data/spec/legion/extensions/llm/conformance/conformance.rb +10 -2
  77. data/spec/legion/extensions/llm/conformance/fixtures/canonical_fleet_round_trip.json +1 -1
  78. data/spec/legion/extensions/llm/conformance/fixtures/canonical_thinking_request.json +2 -2
  79. data/spec/legion/extensions/llm/conformance/provider_translator_examples.rb +1 -1
  80. data/spec/legion/extensions/llm/conformance/ssot_contract_conformance_spec.rb +128 -0
  81. data/spec/legion/extensions/llm/conformance/ssot_contract_examples.rb +505 -0
  82. data/spec/legion/extensions/llm/conformance/ssot_provider_examples.rb +11 -10
  83. data/spec/legion/extensions/llm/credential_sources_spec.rb +12 -13
  84. data/spec/legion/extensions/llm/error_spec.rb +2 -12
  85. data/spec/legion/extensions/llm/fleet/exact_offering_spec.rb +59 -45
  86. data/spec/legion/extensions/llm/fleet/provider_responder_spec.rb +173 -95
  87. data/spec/legion/extensions/llm/fleet/token_validator_spec.rb +7 -2
  88. data/spec/legion/extensions/llm/fleet/worker_execution_spec.rb +93 -74
  89. data/spec/legion/extensions/llm/fleet_messages_spec.rb +119 -125
  90. data/spec/legion/extensions/llm/gemspec_spec.rb +1 -2
  91. data/spec/legion/extensions/llm/inventory/boot_spec.rb +4 -4
  92. data/spec/legion/extensions/llm/inventory/identity_spec.rb +127 -110
  93. data/spec/legion/extensions/llm/inventory/probe_token_spec.rb +4 -4
  94. data/spec/legion/extensions/llm/inventory/publisher_spec.rb +8 -61
  95. data/spec/legion/extensions/llm/inventory/records_spec.rb +82 -47
  96. data/spec/legion/extensions/llm/inventory/registry_activation_spec.rb +49 -16
  97. data/spec/legion/extensions/llm/inventory/registry_replacement_spec.rb +5 -4
  98. data/spec/legion/extensions/llm/inventory/snapshot_spec.rb +11 -5
  99. data/spec/legion/extensions/llm/inventory/weight_reconciler_spec.rb +7 -3
  100. data/spec/legion/extensions/llm/inventory/weight_schema_spec.rb +60 -10
  101. data/spec/legion/extensions/llm/provider/open_ai_compatible_spec.rb +128 -68
  102. data/spec/legion/extensions/llm/provider/open_ai_compatible_tool_calls_array_spec.rb +7 -31
  103. data/spec/legion/extensions/llm/provider_contract_spec.rb +10 -15
  104. data/spec/legion/extensions/llm/provider_spec.rb +97 -78
  105. data/spec/legion/extensions/llm/routing/records_spec.rb +38 -5
  106. data/spec/legion/extensions/llm/stream_accumulator_spec.rb +174 -144
  107. data/spec/legion/extensions/llm/streaming_spec.rb +27 -0
  108. data/spec/legion/extensions/llm/taxonomies_spec.rb +43 -43
  109. data/spec/legion/extensions/llm/transport/fleet_lane_spec.rb +1 -1
  110. data/spec/legion/extensions/llm/utils_spec.rb +26 -7
  111. data/spec/legion/extensions/llm_base_contract_spec.rb +55 -90
  112. data/spec/legion/extensions/llm_extension_spec.rb +5 -5
  113. data/spec/support/fake_llm_provider.rb +45 -39
  114. data/spec/support/fake_ssot_harness.rb +7 -2
  115. metadata +13 -54
  116. data/lib/legion/extensions/llm/agent.rb +0 -366
  117. data/lib/legion/extensions/llm/aliases.json +0 -436
  118. data/lib/legion/extensions/llm/aliases.rb +0 -67
  119. data/lib/legion/extensions/llm/attachment.rb +0 -229
  120. data/lib/legion/extensions/llm/chat.rb +0 -354
  121. data/lib/legion/extensions/llm/chunk.rb +0 -10
  122. data/lib/legion/extensions/llm/content.rb +0 -81
  123. data/lib/legion/extensions/llm/context.rb +0 -33
  124. data/lib/legion/extensions/llm/embedding.rb +0 -33
  125. data/lib/legion/extensions/llm/image.rb +0 -109
  126. data/lib/legion/extensions/llm/inventory/capabilities.rb +0 -40
  127. data/lib/legion/extensions/llm/inventory/scoped_refresher.rb +0 -310
  128. data/lib/legion/extensions/llm/message.rb +0 -118
  129. data/lib/legion/extensions/llm/mime_type.rb +0 -75
  130. data/lib/legion/extensions/llm/model/info.rb +0 -286
  131. data/lib/legion/extensions/llm/model/modalities.rb +0 -26
  132. data/lib/legion/extensions/llm/model/pricing.rb +0 -52
  133. data/lib/legion/extensions/llm/model/pricing_category.rb +0 -50
  134. data/lib/legion/extensions/llm/model/pricing_tier.rb +0 -37
  135. data/lib/legion/extensions/llm/model.rb +0 -11
  136. data/lib/legion/extensions/llm/models.json +0 -57313
  137. data/lib/legion/extensions/llm/models.rb +0 -530
  138. data/lib/legion/extensions/llm/models_schema.json +0 -168
  139. data/lib/legion/extensions/llm/moderation.rb +0 -60
  140. data/lib/legion/extensions/llm/registry_event_builder.rb +0 -141
  141. data/lib/legion/extensions/llm/registry_publisher.rb +0 -107
  142. data/lib/legion/extensions/llm/responses/chat_response.rb +0 -43
  143. data/lib/legion/extensions/llm/responses/embedding_response.rb +0 -38
  144. data/lib/legion/extensions/llm/responses/stream_chunk.rb +0 -43
  145. data/lib/legion/extensions/llm/routing/lane_key.rb +0 -66
  146. data/lib/legion/extensions/llm/routing/model_offering.rb +0 -241
  147. data/lib/legion/extensions/llm/routing/offering_registry.rb +0 -101
  148. data/lib/legion/extensions/llm/routing/registry_event.rb +0 -167
  149. data/lib/legion/extensions/llm/thinking.rb +0 -53
  150. data/lib/legion/extensions/llm/tokens.rb +0 -51
  151. data/lib/legion/extensions/llm/tool_call.rb +0 -34
  152. data/lib/legion/extensions/llm/transcription.rb +0 -39
  153. data/lib/legion/extensions/llm/transport/messages/registry_event.rb +0 -44
  154. data/spec/legion/extensions/llm/agent_spec.rb +0 -179
  155. data/spec/legion/extensions/llm/attachment_spec.rb +0 -25
  156. data/spec/legion/extensions/llm/conformance/fixtures/ssot_identity_vectors.json +0 -84
  157. data/spec/legion/extensions/llm/context_spec.rb +0 -127
  158. data/spec/legion/extensions/llm/inventory/capabilities_spec.rb +0 -43
  159. data/spec/legion/extensions/llm/inventory/scoped_refresher_spec.rb +0 -340
  160. data/spec/legion/extensions/llm/message_spec.rb +0 -64
  161. data/spec/legion/extensions/llm/model/info_spec.rb +0 -222
  162. data/spec/legion/extensions/llm/models_spec.rb +0 -104
  163. data/spec/legion/extensions/llm/registry_event_builder_spec.rb +0 -68
  164. data/spec/legion/extensions/llm/registry_publisher_spec.rb +0 -22
  165. data/spec/legion/extensions/llm/responses/response_objects_spec.rb +0 -75
  166. data/spec/legion/extensions/llm/routing/model_offering_spec.rb +0 -281
  167. data/spec/legion/extensions/llm/routing/offering_registry_spec.rb +0 -50
  168. data/spec/legion/extensions/llm/routing/registry_event_spec.rb +0 -120
@@ -5,106 +5,71 @@ require 'spec_helper'
5
5
  RSpec.describe Legion::Extensions::Llm do
6
6
  include_context 'with fake llm provider'
7
7
 
8
- before do
9
- stub_const(
10
- 'SpecSupport::EchoTool',
11
- Class.new(Legion::Extensions::Llm::Tool) do
12
- description 'Echo a numeric value'
13
- param :value, type: :integer, desc: 'Value to echo'
14
-
15
- def execute(value:)
16
- "echo #{value}"
17
- end
18
- end
19
- )
20
- end
21
-
22
- it 'loads and discovers provider classes from the namespace' do
23
- provider_classes = Legion::Extensions::Llm::Models.scan_provider_classes
24
- expect(provider_classes).to include(fake_llm: SpecSupport::FakeLLMProvider)
25
- expect(Legion::Extensions::Llm::Routing::ModelOffering).to be_a(Class)
26
- end
27
-
28
- it 'lets a provider gem register options and satisfy chat through the shared API' do
29
- chat = described_class.chat(model: 'fake-chat-model', provider: :fake_llm, assume_model_exists: true)
30
- response = chat.ask('hello')
31
-
32
- expect(response).to have_attributes(
33
- role: :assistant,
34
- content: 'fake response to hello',
35
- model_id: 'fake-chat-model',
36
- input_tokens: 10,
37
- output_tokens: 5
38
- )
39
- end
40
-
41
- it 'runs shared tool orchestration without provider-specific payload code' do
42
- response = described_class.chat(model: 'fake-chat-model', provider: :fake_llm, assume_model_exists: true)
43
- .with_tool(SpecSupport::EchoTool)
44
- .ask('use the tool')
45
-
46
- expect(response.content).to eq('tool result: echo 21')
8
+ it 'exposes the AutoRegistration discovery contract from the provider namespace' do
9
+ # Registration into Legion::LLM is owned by legion-llm (R12); lex-llm owns
10
+ # the namespace contract the scanner reads: PROVIDER_FAMILY (Symbol) plus
11
+ # a provider_class singleton returning the Provider subclass.
12
+ expect(Legion::Extensions::Llm::FakeLlmProvider.const_get(:PROVIDER_FAMILY)).to eq(:fake_llm)
13
+ expect(Legion::Extensions::Llm::FakeLlmProvider.provider_class).to be < Legion::Extensions::Llm::Provider
47
14
  end
48
15
 
49
- it 'normalizes schema responses through the base chat layer' do
50
- schema = {
51
- name: 'answer',
52
- schema: {
53
- type: 'object',
54
- properties: { answer: { type: 'integer' } },
55
- required: ['answer']
56
- }
57
- }
58
-
59
- response = described_class.chat(model: 'fake-chat-model', provider: :fake_llm, assume_model_exists: true)
60
- .with_schema(schema)
61
- .ask('structured')
62
-
63
- expect(response.content).to eq({ 'answer' => 42 })
64
- end
16
+ describe 'provider funnel contract (0.8.0)' do
17
+ let(:provider) do
18
+ SpecSupport::FakeLLMProvider.new(
19
+ Legion::Extensions::Llm::Configuration.new.tap do |config|
20
+ config.fake_llm_api_key = 'fake-key'
21
+ end
22
+ )
23
+ end
65
24
 
66
- it 'delegates embedding, moderation, image, and transcription calls through registered providers' do
67
- expect(described_class.embed('hello', model: 'fake-embed', provider: :fake_llm, assume_model_exists: true).vectors)
68
- .to eq([0.5, 0.5, 0.5])
25
+ let(:user_message) do
26
+ Legion::Extensions::Llm::Canonical::Message.build(role: :user, content: 'hello')
27
+ end
69
28
 
70
- expect(described_class.moderate('safe', model: 'fake-moderation', provider: :fake_llm, assume_model_exists: true))
71
- .not_to be_flagged
29
+ it 'completes with canonical input and returns a Canonical::Response' do
30
+ response = provider.complete([user_message], model: 'fake-chat-model')
31
+ expect(response).to be_a(Legion::Extensions::Llm::Canonical::Response)
32
+ expect(response.text).to eq('fake response to hello')
33
+ expect(response.usage).to be_a(Legion::Extensions::Llm::Canonical::Usage)
34
+ end
72
35
 
73
- expect(described_class.paint('draw', model: 'fake-image', provider: :fake_llm, assume_model_exists: true).to_blob)
74
- .to eq('fake-image')
36
+ it 'streams canonical chunks ending in exactly one done chunk (05 O5)' do
37
+ chunks = []
38
+ provider.complete([user_message], model: 'fake-chat-model') { |chunk| chunks << chunk }
75
39
 
76
- expect(described_class.transcribe('audio.wav', model: 'fake-audio', provider: :fake_llm,
77
- assume_model_exists: true).text)
78
- .to eq('fake transcript')
79
- end
80
-
81
- it 'connects model offerings to Legion fleet queue construction end to end' do
82
- offering = Legion::Extensions::Llm::Routing::ModelOffering.new(
83
- provider_family: :fake_llm,
84
- instance_id: :worker_one,
85
- transport: :rabbitmq,
86
- model: 'fake-chat-model',
87
- limits: { context_window: 16_384 }
88
- )
89
- exchange_class = Class.new
90
- base_queue = Class.new do
91
- attr_reader :bindings
40
+ expect(chunks).to all(be_a(Legion::Extensions::Llm::Canonical::Chunk))
41
+ expect(chunks.count(&:done?)).to eq(1)
42
+ expect(chunks.last).to be_done
43
+ expect(chunks.filter_map(&:delta).join).to eq('streamed response')
44
+ end
92
45
 
93
- def initialize
94
- @bindings = []
95
- end
46
+ it 'rejects non-canonical messages at the boundary with a typed ArgumentError (08 F2)' do
47
+ expect { provider.complete([{ role: 'user', content: 'hash' }], model: 'fake-chat-model') }
48
+ .to raise_error(ArgumentError, /Canonical::Message/)
49
+ expect { provider.complete([nil], model: 'fake-chat-model') }
50
+ .to raise_error(ArgumentError, /Canonical::Message/)
51
+ expect { provider.complete(['string'], model: 'fake-chat-model') }
52
+ .to raise_error(ArgumentError, /Canonical::Message/)
53
+ end
96
54
 
97
- def bind(exchange, routing_key:)
98
- @bindings << [exchange, routing_key]
99
- end
55
+ it 'keeps the canonical conversation operations on the base' do
56
+ expect(provider).to respond_to(:chat)
57
+ expect(provider).to respond_to(:stream_chat)
58
+ expect(provider).to respond_to(:count_tokens)
59
+ expect(provider).to respond_to(:discover_offerings)
60
+ expect(provider).to respond_to(:health)
61
+ expect(provider).to respond_to(:embed)
62
+ expect(provider).to respond_to(:moderate)
63
+ expect(provider).to respond_to(:image)
64
+ expect(provider).to respond_to(:transcribe)
100
65
  end
101
66
 
102
- queue_class = Legion::Extensions::Llm::Transport::FleetLane.build_queue_class(
103
- queue_name: offering.lane_key,
104
- exchange_class: exchange_class,
105
- base_queue_class: base_queue
106
- )
67
+ it 'serves discover_offerings from the registry snapshot (07 C5)' do
68
+ expect(provider.discover_offerings).to eq([])
69
+ end
107
70
 
108
- expect(queue_class.new.queue_name).to eq('llm.fleet.inference.fake-chat-model.ctx16384')
71
+ it 'counts tokens for canonical messages' do
72
+ expect(provider.count_tokens(messages: [user_message], model: 'fake-chat-model')).to be_a(Integer)
73
+ end
109
74
  end
110
75
  end
@@ -3,11 +3,11 @@
3
3
  require 'legion/extensions/llm'
4
4
 
5
5
  RSpec.describe Legion::Extensions::Llm do
6
- it 'exposes the Legion-native extension namespace for autoloading' do
7
- expect(described_class::Types::ModelOffering).to equal(Legion::Extensions::Llm::Routing::ModelOffering)
8
- expect(described_class::Types::OfferingRegistry).to equal(Legion::Extensions::Llm::Routing::OfferingRegistry)
9
- expect(described_class::Routing::LaneKey).to equal(Legion::Extensions::Llm::Routing::LaneKey)
10
- expect(described_class::Routing::OfferingRegistry).to equal(Legion::Extensions::Llm::Routing::OfferingRegistry)
6
+ it 'exposes no legacy dual-constant paths after the 0.8.0 rip' do
7
+ expect(described_class.const_defined?(:Types, false)).to be(false)
8
+ expect(Legion::Extensions::Llm::Routing.const_defined?(:ModelOffering, false)).to be(false)
9
+ expect(Legion::Extensions::Llm::Routing.const_defined?(:OfferingRegistry, false)).to be(false)
10
+ expect(Legion::Extensions::Llm::Routing.const_defined?(:LaneKey, false)).to be(false)
11
11
  end
12
12
 
13
13
  it 'provides complete default fleet settings' do
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SpecSupport
4
+ # Contract-conformant fake provider (0.8.0): canonical in, canonical out.
5
+ # No HTTP — complete is overridden to return canned Canonical::Response
6
+ # objects (and yield Canonical::Chunk for streams).
4
7
  class FakeLLMProvider < Legion::Extensions::Llm::Provider
5
8
  class << self
6
9
  def configuration_options
@@ -29,26 +32,28 @@ module SpecSupport
29
32
  config.fake_llm_api_base || 'https://fake-llm.invalid/v1'
30
33
  end
31
34
 
32
- # rubocop:disable Metrics/ParameterLists, Lint/UnusedMethodArgument
33
- def complete(messages, tools:, temperature:, model:, params: {}, headers: {}, schema: nil, thinking: nil,
34
- tool_prefs: nil)
35
+ def complete(messages, tools: [], model:, params: nil, headers: {}, schema: nil, thinking: nil, # rubocop:disable Metrics/ParameterLists, Lint/UnusedMethodArgument, Style/KeywordParametersOrder
36
+ tool_prefs: nil, &) # rubocop:disable Lint/UnusedMethodArgument
37
+ enforce_canonical_messages!(messages)
38
+
35
39
  if block_given?
36
- yield Legion::Extensions::Llm::Chunk.new(content: 'streamed ', role: :assistant)
37
- yield Legion::Extensions::Llm::Chunk.new(content: 'response', role: :assistant)
40
+ yield Legion::Extensions::Llm::Canonical::Chunk.text_delta(delta: 'streamed ', request_id: nil)
41
+ yield Legion::Extensions::Llm::Canonical::Chunk.text_delta(delta: 'response', request_id: nil)
42
+ yield Legion::Extensions::Llm::Canonical::Chunk.done(request_id: nil, stop_reason: :end_turn)
38
43
  end
39
44
 
40
- if tools.any? && messages.none?(&:tool_result?)
45
+ has_tool_result = messages.any?(&:tool_call_id)
46
+ if tools.any? && !has_tool_result
41
47
  tool = tools.values.first
42
- return Legion::Extensions::Llm::Message.new(
43
- role: :assistant,
44
- content: nil,
45
- model_id: model.id,
46
- tool_calls: {
47
- tool.name.to_sym => Legion::Extensions::Llm::ToolCall.new(id: 'tool-call-1', name: tool.name,
48
- arguments: { value: 21 })
49
- },
50
- input_tokens: 12,
51
- output_tokens: 3
48
+ return Legion::Extensions::Llm::Canonical::Response.build(
49
+ text: nil,
50
+ tool_calls: [
51
+ Legion::Extensions::Llm::Canonical::ToolCall.build(name: tool.name, id: 'tool-call-1',
52
+ arguments: { value: 21 })
53
+ ],
54
+ stop_reason: :tool_use,
55
+ model: model,
56
+ usage: Legion::Extensions::Llm::Canonical::Usage.build(input_tokens: 12, output_tokens: 3)
52
57
  )
53
58
  end
54
59
 
@@ -56,45 +61,46 @@ module SpecSupport
56
61
  Legion::JSON.generate({ answer: 42 })
57
62
  elsif thinking
58
63
  'fake response with thinking enabled'
59
- elsif messages.any?(&:tool_result?)
60
- "tool result: #{messages.last.content}"
64
+ elsif has_tool_result
65
+ "tool result: #{messages.last.text}"
61
66
  else
62
- "fake response to #{messages.last.content}"
67
+ "fake response to #{messages.last.text}"
63
68
  end
64
69
 
65
- Legion::Extensions::Llm::Message.new(
66
- role: :assistant,
67
- content: content,
68
- model_id: model.id,
69
- input_tokens: 10,
70
- output_tokens: 5
70
+ Legion::Extensions::Llm::Canonical::Response.build(
71
+ text: content,
72
+ model: model,
73
+ stop_reason: :end_turn,
74
+ usage: Legion::Extensions::Llm::Canonical::Usage.build(input_tokens: 10, output_tokens: 5)
71
75
  )
72
76
  end
73
- # rubocop:enable Metrics/ParameterLists, Lint/UnusedMethodArgument
74
77
 
75
- def embed(text:, model:, dimensions: nil)
78
+ def embed(text:, model:, dimensions: nil, params: nil, headers: {})
79
+ _ = [params, headers]
76
80
  size = dimensions || 3
77
81
  vectors = Array(text).map { Array.new(size, 0.5) }
78
82
  vectors = vectors.first unless text.is_a?(Array)
79
83
 
80
- Legion::Extensions::Llm::Embedding.new(vectors: vectors, model: model, input_tokens: Array(text).size)
84
+ {
85
+ text: text,
86
+ model: model,
87
+ embedding: vectors,
88
+ usage: Legion::Extensions::Llm::Canonical::Usage.build(input_tokens: Array(text).size)
89
+ }
81
90
  end
82
91
 
83
- def moderate(_input, model:)
84
- Legion::Extensions::Llm::Moderation.new(
85
- id: 'moderation-1',
86
- model: model,
87
- results: [{ 'flagged' => false, 'categories' => {}, 'category_scores' => {} }]
88
- )
92
+ def moderate(input:, model:)
93
+ _ = input
94
+ { model: model, result: { flagged: false, categories: {} } }
89
95
  end
90
96
 
91
- def paint(_prompt, model:, size:, with: nil, mask: nil, params: {}) # rubocop:disable Metrics/ParameterLists, Lint/UnusedMethodArgument
92
- Legion::Extensions::Llm::Image.new(data: Base64.strict_encode64('fake-image'), mime_type: 'image/png',
93
- model_id: model)
97
+ def image(prompt:, model:, size:, with: nil, mask: nil, params: {}) # rubocop:disable Metrics/ParameterLists
98
+ _ = [prompt, with, mask, params]
99
+ { model: model, image: Base64.strict_encode64('fake-image'), size: }
94
100
  end
95
101
 
96
102
  def transcribe(_audio_file, model:, language:, **)
97
- Legion::Extensions::Llm::Transcription.new(text: 'fake transcript', model: model, language: language)
103
+ { model: model, text: 'fake transcript', language: }
98
104
  end
99
105
 
100
106
  # Records exactly-once disconnect for the callable-lifecycle contract; does
@@ -123,7 +129,7 @@ module SpecSupport
123
129
  end
124
130
 
125
131
  # Namespace module that mimics a real lex-llm-* provider extension.
126
- # Models.scan_provider_classes discovers providers via this pattern.
132
+ # legion-llm's Call::Providers scanner discovers providers via this pattern.
127
133
  module Legion
128
134
  module Extensions
129
135
  module Llm
@@ -28,9 +28,14 @@ module SpecSupport
28
28
  true
29
29
  end
30
30
 
31
- def chat(messages:, model:, **rest)
31
+ # L11: the fake mimics the 0.8.0 callable contract — chat returns a
32
+ # Canonical::Response (the raw-Hash return violated the contract the
33
+ # harness impersonates; conformance B2 asserts exactly this shape).
34
+ def chat(messages, model:, **rest)
32
35
  @inference_calls += 1
33
- { content: "fake chat #{model} #{messages.size} #{rest.size}" }
36
+ Legion::Extensions::Llm::Canonical::Response.build(
37
+ text: "fake chat #{model} #{messages.size} #{rest.size}", model: model, stop_reason: :end_turn
38
+ )
34
39
  end
35
40
 
36
41
  # Runtime error normalization on the provider callable itself, mirroring the
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-llm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.6
4
+ version: 0.8.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - LegionIO
@@ -253,13 +253,10 @@ files:
253
253
  - Gemfile
254
254
  - LICENSE
255
255
  - README.md
256
+ - RULES.md
256
257
  - lex-llm.gemspec
257
258
  - lib/legion/extensions/llm.rb
258
259
  - lib/legion/extensions/llm/actors/metering_flush.rb
259
- - lib/legion/extensions/llm/agent.rb
260
- - lib/legion/extensions/llm/aliases.json
261
- - lib/legion/extensions/llm/aliases.rb
262
- - lib/legion/extensions/llm/attachment.rb
263
260
  - lib/legion/extensions/llm/auto_registration.rb
264
261
  - lib/legion/extensions/llm/canonical.rb
265
262
  - lib/legion/extensions/llm/canonical/chunk.rb
@@ -268,25 +265,26 @@ files:
268
265
  - lib/legion/extensions/llm/canonical/params.rb
269
266
  - lib/legion/extensions/llm/canonical/request.rb
270
267
  - lib/legion/extensions/llm/canonical/response.rb
268
+ - lib/legion/extensions/llm/canonical/strict.rb
271
269
  - lib/legion/extensions/llm/canonical/thinking.rb
270
+ - lib/legion/extensions/llm/canonical/thinking_config.rb
272
271
  - lib/legion/extensions/llm/canonical/tool_call.rb
273
272
  - lib/legion/extensions/llm/canonical/tool_definition.rb
274
273
  - lib/legion/extensions/llm/canonical/tool_schema.rb
275
274
  - lib/legion/extensions/llm/canonical/usage.rb
276
275
  - lib/legion/extensions/llm/capabilities.rb
277
276
  - lib/legion/extensions/llm/capability_policy.rb
278
- - lib/legion/extensions/llm/chat.rb
279
- - lib/legion/extensions/llm/chunk.rb
280
277
  - lib/legion/extensions/llm/configuration.rb
281
278
  - lib/legion/extensions/llm/connection.rb
282
- - lib/legion/extensions/llm/content.rb
283
- - lib/legion/extensions/llm/context.rb
284
279
  - lib/legion/extensions/llm/credential_sources.rb
285
- - lib/legion/extensions/llm/embedding.rb
280
+ - lib/legion/extensions/llm/discovery/actor.rb
281
+ - lib/legion/extensions/llm/discovery/pipeline.rb
286
282
  - lib/legion/extensions/llm/error.rb
287
283
  - lib/legion/extensions/llm/errors/unsupported_capability.rb
284
+ - lib/legion/extensions/llm/fleet/contract_error.rb
288
285
  - lib/legion/extensions/llm/fleet/default_exchange_reply.rb
289
286
  - lib/legion/extensions/llm/fleet/envelope_validation.rb
287
+ - lib/legion/extensions/llm/fleet/fleet_envelope.rb
290
288
  - lib/legion/extensions/llm/fleet/protocol.rb
291
289
  - lib/legion/extensions/llm/fleet/provider_responder.rb
292
290
  - lib/legion/extensions/llm/fleet/publish_safety.rb
@@ -294,9 +292,7 @@ files:
294
292
  - lib/legion/extensions/llm/fleet/token_error.rb
295
293
  - lib/legion/extensions/llm/fleet/token_validator.rb
296
294
  - lib/legion/extensions/llm/fleet/worker_execution.rb
297
- - lib/legion/extensions/llm/image.rb
298
295
  - lib/legion/extensions/llm/inventory/callable_handle.rb
299
- - lib/legion/extensions/llm/inventory/capabilities.rb
300
296
  - lib/legion/extensions/llm/inventory/errors.rb
301
297
  - lib/legion/extensions/llm/inventory/evidence.rb
302
298
  - lib/legion/extensions/llm/inventory/identity.rb
@@ -306,56 +302,30 @@ files:
306
302
  - lib/legion/extensions/llm/inventory/publisher.rb
307
303
  - lib/legion/extensions/llm/inventory/records.rb
308
304
  - lib/legion/extensions/llm/inventory/registry.rb
309
- - lib/legion/extensions/llm/inventory/scoped_refresher.rb
310
305
  - lib/legion/extensions/llm/inventory/snapshot.rb
311
306
  - lib/legion/extensions/llm/inventory/weight_reconciler.rb
312
307
  - lib/legion/extensions/llm/inventory/weight_schema.rb
313
- - lib/legion/extensions/llm/message.rb
314
- - lib/legion/extensions/llm/mime_type.rb
315
- - lib/legion/extensions/llm/model.rb
316
- - lib/legion/extensions/llm/model/info.rb
317
- - lib/legion/extensions/llm/model/modalities.rb
318
- - lib/legion/extensions/llm/model/pricing.rb
319
- - lib/legion/extensions/llm/model/pricing_category.rb
320
- - lib/legion/extensions/llm/model/pricing_tier.rb
321
- - lib/legion/extensions/llm/models.json
322
- - lib/legion/extensions/llm/models.rb
323
- - lib/legion/extensions/llm/models_schema.json
324
- - lib/legion/extensions/llm/moderation.rb
325
308
  - lib/legion/extensions/llm/provider.rb
326
309
  - lib/legion/extensions/llm/provider/open_ai_compatible.rb
327
310
  - lib/legion/extensions/llm/provider_contract.rb
328
311
  - lib/legion/extensions/llm/provider_settings.rb
329
- - lib/legion/extensions/llm/registry_event_builder.rb
330
- - lib/legion/extensions/llm/registry_publisher.rb
331
- - lib/legion/extensions/llm/responses/chat_response.rb
332
- - lib/legion/extensions/llm/responses/embedding_response.rb
333
- - lib/legion/extensions/llm/responses/stream_chunk.rb
334
312
  - lib/legion/extensions/llm/responses/thinking_extractor.rb
313
+ - lib/legion/extensions/llm/responses/tool_arguments.rb
335
314
  - lib/legion/extensions/llm/routing.rb
336
- - lib/legion/extensions/llm/routing/lane_key.rb
337
- - lib/legion/extensions/llm/routing/model_offering.rb
338
- - lib/legion/extensions/llm/routing/offering_registry.rb
339
315
  - lib/legion/extensions/llm/routing/provider_outcome.rb
340
316
  - lib/legion/extensions/llm/routing/records.rb
341
- - lib/legion/extensions/llm/routing/registry_event.rb
342
317
  - lib/legion/extensions/llm/settings_cascade.rb
343
318
  - lib/legion/extensions/llm/stop_reason_mapping.rb
344
319
  - lib/legion/extensions/llm/stream_accumulator.rb
345
320
  - lib/legion/extensions/llm/streaming.rb
346
321
  - lib/legion/extensions/llm/taxonomies.rb
347
- - lib/legion/extensions/llm/thinking.rb
348
- - lib/legion/extensions/llm/tokens.rb
349
322
  - lib/legion/extensions/llm/tool.rb
350
- - lib/legion/extensions/llm/tool_call.rb
351
- - lib/legion/extensions/llm/transcription.rb
352
323
  - lib/legion/extensions/llm/transport/exchanges/fleet.rb
353
324
  - lib/legion/extensions/llm/transport/exchanges/llm_registry.rb
354
325
  - lib/legion/extensions/llm/transport/fleet_lane.rb
355
326
  - lib/legion/extensions/llm/transport/messages/fleet_error.rb
356
327
  - lib/legion/extensions/llm/transport/messages/fleet_request.rb
357
328
  - lib/legion/extensions/llm/transport/messages/fleet_response.rb
358
- - lib/legion/extensions/llm/transport/messages/registry_event.rb
359
329
  - lib/legion/extensions/llm/utils.rb
360
330
  - lib/legion/extensions/llm/version.rb
361
331
  - spec/fixtures/ruby.mp3
@@ -366,8 +336,6 @@ files:
366
336
  - spec/fixtures/ruby.xml
367
337
  - spec/fixtures/sample.pdf
368
338
  - spec/legion/extensions/llm/actor/metering_flush_spec.rb
369
- - spec/legion/extensions/llm/agent_spec.rb
370
- - spec/legion/extensions/llm/attachment_spec.rb
371
339
  - spec/legion/extensions/llm/auto_registration_spec.rb
372
340
  - spec/legion/extensions/llm/canonical/chunk_spec.rb
373
341
  - spec/legion/extensions/llm/canonical/content_block_spec.rb
@@ -375,6 +343,7 @@ files:
375
343
  - spec/legion/extensions/llm/canonical/params_spec.rb
376
344
  - spec/legion/extensions/llm/canonical/request_spec.rb
377
345
  - spec/legion/extensions/llm/canonical/response_spec.rb
346
+ - spec/legion/extensions/llm/canonical/thinking/config_spec.rb
378
347
  - spec/legion/extensions/llm/canonical/thinking_spec.rb
379
348
  - spec/legion/extensions/llm/canonical/tool_call_spec.rb
380
349
  - spec/legion/extensions/llm/canonical/tool_definition_spec.rb
@@ -383,6 +352,7 @@ files:
383
352
  - spec/legion/extensions/llm/capabilities_spec.rb
384
353
  - spec/legion/extensions/llm/capability_policy_spec.rb
385
354
  - spec/legion/extensions/llm/configuration_spec.rb
355
+ - spec/legion/extensions/llm/conformance/canonical_type_examples.rb
386
356
  - spec/legion/extensions/llm/conformance/client_translator_examples.rb
387
357
  - spec/legion/extensions/llm/conformance/conformance.rb
388
358
  - spec/legion/extensions/llm/conformance/echo_translator.rb
@@ -409,15 +379,15 @@ files:
409
379
  - spec/legion/extensions/llm/conformance/fixtures/canonical_tool_results_continuation_request.json
410
380
  - spec/legion/extensions/llm/conformance/fixtures/canonical_tool_use_response.json
411
381
  - spec/legion/extensions/llm/conformance/fixtures/canonical_tools_request.json
412
- - spec/legion/extensions/llm/conformance/fixtures/ssot_identity_vectors.json
413
382
  - spec/legion/extensions/llm/conformance/provider_tool_rendering_examples.rb
414
383
  - spec/legion/extensions/llm/conformance/provider_translator_examples.rb
384
+ - spec/legion/extensions/llm/conformance/ssot_contract_conformance_spec.rb
385
+ - spec/legion/extensions/llm/conformance/ssot_contract_examples.rb
415
386
  - spec/legion/extensions/llm/conformance/ssot_provider_conformance_spec.rb
416
387
  - spec/legion/extensions/llm/conformance/ssot_provider_examples.rb
417
388
  - spec/legion/extensions/llm/connection_close_spec.rb
418
389
  - spec/legion/extensions/llm/connection_logging_spec.rb
419
390
  - spec/legion/extensions/llm/connection_retry_spec.rb
420
- - spec/legion/extensions/llm/context_spec.rb
421
391
  - spec/legion/extensions/llm/credential_sources_spec.rb
422
392
  - spec/legion/extensions/llm/error_middleware_spec.rb
423
393
  - spec/legion/extensions/llm/error_spec.rb
@@ -429,7 +399,6 @@ files:
429
399
  - spec/legion/extensions/llm/gemspec_spec.rb
430
400
  - spec/legion/extensions/llm/inventory/boot_spec.rb
431
401
  - spec/legion/extensions/llm/inventory/callable_handle_spec.rb
432
- - spec/legion/extensions/llm/inventory/capabilities_spec.rb
433
402
  - spec/legion/extensions/llm/inventory/evidence_spec.rb
434
403
  - spec/legion/extensions/llm/inventory/identity_spec.rb
435
404
  - spec/legion/extensions/llm/inventory/immutable_value_spec.rb
@@ -441,27 +410,17 @@ files:
441
410
  - spec/legion/extensions/llm/inventory/registry_availability_spec.rb
442
411
  - spec/legion/extensions/llm/inventory/registry_callable_spec.rb
443
412
  - spec/legion/extensions/llm/inventory/registry_replacement_spec.rb
444
- - spec/legion/extensions/llm/inventory/scoped_refresher_spec.rb
445
413
  - spec/legion/extensions/llm/inventory/snapshot_spec.rb
446
414
  - spec/legion/extensions/llm/inventory/weight_reconciler_spec.rb
447
415
  - spec/legion/extensions/llm/inventory/weight_schema_spec.rb
448
- - spec/legion/extensions/llm/message_spec.rb
449
- - spec/legion/extensions/llm/model/info_spec.rb
450
- - spec/legion/extensions/llm/models_spec.rb
451
416
  - spec/legion/extensions/llm/provider/open_ai_compatible_spec.rb
452
417
  - spec/legion/extensions/llm/provider/open_ai_compatible_tool_calls_array_spec.rb
453
418
  - spec/legion/extensions/llm/provider_contract_spec.rb
454
419
  - spec/legion/extensions/llm/provider_settings_spec.rb
455
420
  - spec/legion/extensions/llm/provider_spec.rb
456
- - spec/legion/extensions/llm/registry_event_builder_spec.rb
457
- - spec/legion/extensions/llm/registry_publisher_spec.rb
458
- - spec/legion/extensions/llm/responses/response_objects_spec.rb
459
421
  - spec/legion/extensions/llm/responses/thinking_extractor_spec.rb
460
- - spec/legion/extensions/llm/routing/model_offering_spec.rb
461
- - spec/legion/extensions/llm/routing/offering_registry_spec.rb
462
422
  - spec/legion/extensions/llm/routing/provider_outcome_spec.rb
463
423
  - spec/legion/extensions/llm/routing/records_spec.rb
464
- - spec/legion/extensions/llm/routing/registry_event_spec.rb
465
424
  - spec/legion/extensions/llm/settings_cascade_spec.rb
466
425
  - spec/legion/extensions/llm/stop_reason_mapping_spec.rb
467
426
  - spec/legion/extensions/llm/stream_accumulator_spec.rb