lex-llm 0.7.3 → 0.8.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.
Files changed (163) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +106 -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 +106 -92
  6. data/lib/legion/extensions/llm/canonical/content_block.rb +66 -75
  7. data/lib/legion/extensions/llm/canonical/message.rb +52 -67
  8. data/lib/legion/extensions/llm/canonical/params.rb +59 -33
  9. data/lib/legion/extensions/llm/canonical/request.rb +67 -54
  10. data/lib/legion/extensions/llm/canonical/response.rb +61 -76
  11. data/lib/legion/extensions/llm/canonical/strict.rb +105 -0
  12. data/lib/legion/extensions/llm/canonical/thinking.rb +99 -51
  13. data/lib/legion/extensions/llm/canonical/tool_call.rb +42 -51
  14. data/lib/legion/extensions/llm/canonical/tool_definition.rb +56 -46
  15. data/lib/legion/extensions/llm/canonical/tool_schema.rb +15 -22
  16. data/lib/legion/extensions/llm/canonical/usage.rb +47 -40
  17. data/lib/legion/extensions/llm/canonical.rb +6 -5
  18. data/lib/legion/extensions/llm/configuration.rb +40 -10
  19. data/lib/legion/extensions/llm/connection.rb +8 -30
  20. data/lib/legion/extensions/llm/credential_sources.rb +32 -49
  21. data/lib/legion/extensions/llm/discovery/actor.rb +92 -0
  22. data/lib/legion/extensions/llm/discovery/pipeline.rb +604 -0
  23. data/lib/legion/extensions/llm/error.rb +0 -14
  24. data/lib/legion/extensions/llm/fleet/contract_error.rb +15 -0
  25. data/lib/legion/extensions/llm/fleet/envelope_validation.rb +7 -6
  26. data/lib/legion/extensions/llm/fleet/fleet_envelope.rb +66 -0
  27. data/lib/legion/extensions/llm/fleet/protocol.rb +18 -5
  28. data/lib/legion/extensions/llm/fleet/provider_responder.rb +58 -153
  29. data/lib/legion/extensions/llm/fleet/token_validator.rb +15 -21
  30. data/lib/legion/extensions/llm/fleet/worker_execution.rb +107 -150
  31. data/lib/legion/extensions/llm/inventory/errors.rb +0 -1
  32. data/lib/legion/extensions/llm/inventory/evidence.rb +1 -1
  33. data/lib/legion/extensions/llm/inventory/identity.rb +55 -39
  34. data/lib/legion/extensions/llm/inventory/probe_token.rb +9 -12
  35. data/lib/legion/extensions/llm/inventory/publisher.rb +15 -53
  36. data/lib/legion/extensions/llm/inventory/records.rb +82 -99
  37. data/lib/legion/extensions/llm/inventory/registry.rb +54 -46
  38. data/lib/legion/extensions/llm/inventory/snapshot.rb +7 -21
  39. data/lib/legion/extensions/llm/inventory/weight_reconciler.rb +249 -0
  40. data/lib/legion/extensions/llm/inventory/weight_schema.rb +146 -0
  41. data/lib/legion/extensions/llm/provider/open_ai_compatible.rb +172 -182
  42. data/lib/legion/extensions/llm/provider.rb +192 -315
  43. data/lib/legion/extensions/llm/provider_contract.rb +25 -8
  44. data/lib/legion/extensions/llm/provider_settings.rb +5 -26
  45. data/lib/legion/extensions/llm/responses/thinking_extractor.rb +8 -1
  46. data/lib/legion/extensions/llm/responses/tool_arguments.rb +34 -0
  47. data/lib/legion/extensions/llm/routing/provider_outcome.rb +25 -0
  48. data/lib/legion/extensions/llm/routing/records.rb +34 -17
  49. data/lib/legion/extensions/llm/stream_accumulator.rb +186 -270
  50. data/lib/legion/extensions/llm/streaming.rb +50 -35
  51. data/lib/legion/extensions/llm/taxonomies.rb +29 -17
  52. data/lib/legion/extensions/llm/transport/fleet_lane.rb +8 -10
  53. data/lib/legion/extensions/llm/transport/messages/fleet_error.rb +3 -2
  54. data/lib/legion/extensions/llm/transport/messages/fleet_request.rb +6 -8
  55. data/lib/legion/extensions/llm/transport/messages/fleet_response.rb +10 -9
  56. data/lib/legion/extensions/llm/utils.rb +23 -5
  57. data/lib/legion/extensions/llm/version.rb +1 -1
  58. data/lib/legion/extensions/llm.rb +10 -98
  59. data/spec/legion/extensions/llm/auto_registration_spec.rb +4 -9
  60. data/spec/legion/extensions/llm/canonical/chunk_spec.rb +66 -252
  61. data/spec/legion/extensions/llm/canonical/content_block_spec.rb +52 -197
  62. data/spec/legion/extensions/llm/canonical/message_spec.rb +89 -204
  63. data/spec/legion/extensions/llm/canonical/params_spec.rb +57 -136
  64. data/spec/legion/extensions/llm/canonical/request_spec.rb +81 -143
  65. data/spec/legion/extensions/llm/canonical/response_spec.rb +68 -204
  66. data/spec/legion/extensions/llm/canonical/thinking_spec.rb +68 -148
  67. data/spec/legion/extensions/llm/canonical/tool_call_spec.rb +59 -162
  68. data/spec/legion/extensions/llm/canonical/tool_definition_spec.rb +55 -191
  69. data/spec/legion/extensions/llm/canonical/tool_schema_spec.rb +26 -67
  70. data/spec/legion/extensions/llm/canonical/usage_spec.rb +46 -155
  71. data/spec/legion/extensions/llm/configuration_spec.rb +31 -5
  72. data/spec/legion/extensions/llm/conformance/canonical_type_examples.rb +106 -0
  73. data/spec/legion/extensions/llm/conformance/conformance.rb +10 -2
  74. data/spec/legion/extensions/llm/conformance/provider_translator_examples.rb +1 -1
  75. data/spec/legion/extensions/llm/conformance/ssot_contract_conformance_spec.rb +130 -0
  76. data/spec/legion/extensions/llm/conformance/ssot_contract_examples.rb +507 -0
  77. data/spec/legion/extensions/llm/conformance/ssot_provider_examples.rb +11 -10
  78. data/spec/legion/extensions/llm/credential_sources_spec.rb +12 -13
  79. data/spec/legion/extensions/llm/error_spec.rb +2 -12
  80. data/spec/legion/extensions/llm/fleet/exact_offering_spec.rb +59 -45
  81. data/spec/legion/extensions/llm/fleet/provider_responder_spec.rb +178 -67
  82. data/spec/legion/extensions/llm/fleet/token_validator_spec.rb +7 -2
  83. data/spec/legion/extensions/llm/fleet/worker_execution_spec.rb +101 -62
  84. data/spec/legion/extensions/llm/fleet_messages_spec.rb +118 -123
  85. data/spec/legion/extensions/llm/inventory/boot_spec.rb +4 -4
  86. data/spec/legion/extensions/llm/inventory/identity_spec.rb +127 -110
  87. data/spec/legion/extensions/llm/inventory/probe_token_spec.rb +4 -4
  88. data/spec/legion/extensions/llm/inventory/publisher_spec.rb +8 -61
  89. data/spec/legion/extensions/llm/inventory/records_spec.rb +129 -41
  90. data/spec/legion/extensions/llm/inventory/registry_activation_spec.rb +55 -4
  91. data/spec/legion/extensions/llm/inventory/registry_replacement_spec.rb +5 -4
  92. data/spec/legion/extensions/llm/inventory/snapshot_spec.rb +11 -5
  93. data/spec/legion/extensions/llm/inventory/weight_reconciler_spec.rb +312 -0
  94. data/spec/legion/extensions/llm/inventory/weight_schema_spec.rb +141 -0
  95. data/spec/legion/extensions/llm/provider/open_ai_compatible_spec.rb +105 -68
  96. data/spec/legion/extensions/llm/provider/open_ai_compatible_tool_calls_array_spec.rb +7 -31
  97. data/spec/legion/extensions/llm/provider_contract_spec.rb +10 -15
  98. data/spec/legion/extensions/llm/provider_spec.rb +98 -78
  99. data/spec/legion/extensions/llm/routing/records_spec.rb +38 -5
  100. data/spec/legion/extensions/llm/stream_accumulator_spec.rb +174 -144
  101. data/spec/legion/extensions/llm/streaming_spec.rb +27 -0
  102. data/spec/legion/extensions/llm/taxonomies_spec.rb +43 -16
  103. data/spec/legion/extensions/llm/transport/fleet_lane_spec.rb +1 -1
  104. data/spec/legion/extensions/llm/utils_spec.rb +26 -7
  105. data/spec/legion/extensions/llm_base_contract_spec.rb +55 -90
  106. data/spec/legion/extensions/llm_extension_spec.rb +5 -5
  107. data/spec/support/fake_llm_provider.rb +45 -39
  108. data/spec/support/fake_ssot_harness.rb +7 -2
  109. data/spec/support/ssot_registry_helpers.rb +3 -2
  110. metadata +15 -54
  111. data/lib/legion/extensions/llm/agent.rb +0 -366
  112. data/lib/legion/extensions/llm/aliases.json +0 -436
  113. data/lib/legion/extensions/llm/aliases.rb +0 -67
  114. data/lib/legion/extensions/llm/attachment.rb +0 -229
  115. data/lib/legion/extensions/llm/chat.rb +0 -354
  116. data/lib/legion/extensions/llm/chunk.rb +0 -10
  117. data/lib/legion/extensions/llm/content.rb +0 -81
  118. data/lib/legion/extensions/llm/context.rb +0 -33
  119. data/lib/legion/extensions/llm/embedding.rb +0 -33
  120. data/lib/legion/extensions/llm/image.rb +0 -109
  121. data/lib/legion/extensions/llm/inventory/capabilities.rb +0 -40
  122. data/lib/legion/extensions/llm/inventory/scoped_refresher.rb +0 -311
  123. data/lib/legion/extensions/llm/message.rb +0 -118
  124. data/lib/legion/extensions/llm/mime_type.rb +0 -75
  125. data/lib/legion/extensions/llm/model/info.rb +0 -286
  126. data/lib/legion/extensions/llm/model/modalities.rb +0 -26
  127. data/lib/legion/extensions/llm/model/pricing.rb +0 -52
  128. data/lib/legion/extensions/llm/model/pricing_category.rb +0 -50
  129. data/lib/legion/extensions/llm/model/pricing_tier.rb +0 -37
  130. data/lib/legion/extensions/llm/model.rb +0 -11
  131. data/lib/legion/extensions/llm/models.json +0 -57313
  132. data/lib/legion/extensions/llm/models.rb +0 -530
  133. data/lib/legion/extensions/llm/models_schema.json +0 -168
  134. data/lib/legion/extensions/llm/moderation.rb +0 -60
  135. data/lib/legion/extensions/llm/registry_event_builder.rb +0 -141
  136. data/lib/legion/extensions/llm/registry_publisher.rb +0 -107
  137. data/lib/legion/extensions/llm/responses/chat_response.rb +0 -43
  138. data/lib/legion/extensions/llm/responses/embedding_response.rb +0 -38
  139. data/lib/legion/extensions/llm/responses/stream_chunk.rb +0 -43
  140. data/lib/legion/extensions/llm/routing/lane_key.rb +0 -66
  141. data/lib/legion/extensions/llm/routing/model_offering.rb +0 -241
  142. data/lib/legion/extensions/llm/routing/offering_registry.rb +0 -101
  143. data/lib/legion/extensions/llm/routing/registry_event.rb +0 -167
  144. data/lib/legion/extensions/llm/thinking.rb +0 -53
  145. data/lib/legion/extensions/llm/tokens.rb +0 -51
  146. data/lib/legion/extensions/llm/tool_call.rb +0 -34
  147. data/lib/legion/extensions/llm/transcription.rb +0 -39
  148. data/lib/legion/extensions/llm/transport/messages/registry_event.rb +0 -44
  149. data/spec/legion/extensions/llm/agent_spec.rb +0 -179
  150. data/spec/legion/extensions/llm/attachment_spec.rb +0 -25
  151. data/spec/legion/extensions/llm/conformance/fixtures/ssot_identity_vectors.json +0 -84
  152. data/spec/legion/extensions/llm/context_spec.rb +0 -127
  153. data/spec/legion/extensions/llm/inventory/capabilities_spec.rb +0 -43
  154. data/spec/legion/extensions/llm/inventory/scoped_refresher_spec.rb +0 -337
  155. data/spec/legion/extensions/llm/message_spec.rb +0 -64
  156. data/spec/legion/extensions/llm/model/info_spec.rb +0 -222
  157. data/spec/legion/extensions/llm/models_spec.rb +0 -104
  158. data/spec/legion/extensions/llm/registry_event_builder_spec.rb +0 -68
  159. data/spec/legion/extensions/llm/registry_publisher_spec.rb +0 -22
  160. data/spec/legion/extensions/llm/responses/response_objects_spec.rb +0 -75
  161. data/spec/legion/extensions/llm/routing/model_offering_spec.rb +0 -281
  162. data/spec/legion/extensions/llm/routing/offering_registry_spec.rb +0 -50
  163. data/spec/legion/extensions/llm/routing/registry_event_spec.rb +0 -120
@@ -1,44 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- return unless defined?(Legion::Transport::Message)
4
-
5
- require_relative '../exchanges/llm_registry'
6
-
7
- module Legion
8
- module Extensions
9
- module Llm
10
- module Transport
11
- module Messages
12
- # Publishes lex-llm RegistryEvent envelopes to the shared llm.registry exchange.
13
- # Accepts a `provider_family` for constructing the app_id and routing key.
14
- class RegistryEvent < ::Legion::Transport::Message
15
- def initialize(event:, provider_family: nil, **options)
16
- @provider_family = provider_family
17
- super(**event.to_h.merge(options))
18
- end
19
-
20
- def exchange
21
- Exchanges::LlmRegistry
22
- end
23
-
24
- def routing_key
25
- @options[:routing_key] || "llm.registry.#{@options.fetch(:event_type)}"
26
- end
27
-
28
- def type
29
- 'llm.registry.event'
30
- end
31
-
32
- def app_id
33
- @options[:app_id] || "lex-llm-#{@provider_family || 'unknown'}"
34
- end
35
-
36
- def persistent # rubocop:disable Naming/PredicateMethod
37
- false
38
- end
39
- end
40
- end
41
- end
42
- end
43
- end
44
- end
@@ -1,179 +0,0 @@
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
@@ -1,25 +0,0 @@
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
@@ -1,84 +0,0 @@
1
- {
2
- "description": "Binding SSOT v3 identity input/byte/digest fixtures. Recompute every field and digest; do not merely check a prefix. No Ruby object hash, hash-iteration order, or delimiter splitting is used anywhere.",
3
- "algorithm": {
4
- "frame": "binary bytes: decimal(UTF-8 byte length) + ASCII 0x3a colon + exact UTF-8 bytes",
5
- "offering_id": "off:v1: + lowercase_hex(SHA256(frame(provider_family) + frame(instance_id) + frame(provider_native_key)))",
6
- "lane_id": "lane:v1: + lowercase_hex(SHA256(ASCII(\"lane-v1\\u0000\") + frame(provider_family) + frame(instance_id) + frame(operation) + frame(model) + frame(offering_id)))",
7
- "lane_domain_prefix_hex": "6c616e652d763100",
8
- "notes": "Tier is never an identity input. provider_native_key, model, and offering_id preserve declared case."
9
- },
10
- "vectors": [
11
- {
12
- "provider_family": "vllm",
13
- "instance_id": "h200",
14
- "provider_native_key": "gemma4",
15
- "model": "gemma4",
16
- "operation": "chat",
17
- "framed_hex": {
18
- "provider_family": "343a766c6c6d",
19
- "instance_id": "343a68323030",
20
- "provider_native_key": "363a67656d6d6134",
21
- "operation": "343a63686174",
22
- "model": "363a67656d6d6134",
23
- "offering_id": "37313a6f66663a76313a63393636373732643766386634323863323461373762653936663934653131316261313035326266303933303938663034313362353632633130353964636431"
24
- },
25
- "offering_hash_input_hex": "343a766c6c6d343a68323030363a67656d6d6134",
26
- "expected_offering_id": "off:v1:c966772d7f8f428c24a77be96f94e111ba1052bf093098f0413b562c1059dcd1",
27
- "lane_hash_input_hex": "6c616e652d763100343a766c6c6d343a68323030343a63686174363a67656d6d613437313a6f66663a76313a63393636373732643766386634323863323461373762653936663934653131316261313035326266303933303938663034313362353632633130353964636431",
28
- "expected_lane_id": "lane:v1:430444bb58975ab56329c7c3a6c483a1b9cb49d1c089a56f3c93c6aa27d5f9b3"
29
- },
30
- {
31
- "provider_family": "vllm",
32
- "instance_id": "helios1",
33
- "provider_native_key": "gemma4",
34
- "model": "gemma4",
35
- "operation": "chat",
36
- "framed_hex": {
37
- "provider_family": "343a766c6c6d",
38
- "instance_id": "373a68656c696f7331",
39
- "provider_native_key": "363a67656d6d6134",
40
- "operation": "343a63686174",
41
- "model": "363a67656d6d6134",
42
- "offering_id": "37313a6f66663a76313a66336333316366313830313739363436343163386166656463656261316136366135326661626630313463666632623365316438383165333639376339656334"
43
- },
44
- "offering_hash_input_hex": "343a766c6c6d373a68656c696f7331363a67656d6d6134",
45
- "expected_offering_id": "off:v1:f3c31cf18017964641c8afedceba1a66a52fabf014cff2b3e1d881e3697c9ec4",
46
- "lane_hash_input_hex": "6c616e652d763100343a766c6c6d373a68656c696f7331343a63686174363a67656d6d613437313a6f66663a76313a66336333316366313830313739363436343163386166656463656261316136366135326661626630313463666632623365316438383165333639376339656334",
47
- "expected_lane_id": "lane:v1:e4a99f7884ea1e9c36b0a0e62da5038832857f0e4b69234133faa81b0e30e880"
48
- },
49
- {
50
- "provider_family": "azure_foundry",
51
- "instance_id": "prod:eastus",
52
- "provider_native_key": "deploy:gpt-4o",
53
- "model": "gpt-4o",
54
- "operation": "chat",
55
- "framed_hex": {
56
- "provider_family": "31333a617a7572655f666f756e647279",
57
- "instance_id": "31313a70726f643a656173747573",
58
- "provider_native_key": "31333a6465706c6f793a6770742d346f",
59
- "operation": "343a63686174",
60
- "model": "363a6770742d346f",
61
- "offering_id": "37313a6f66663a76313a37646431346639386164346266656335316666623136396561323064633137616564316562653033613363396664666365383966383638343534633035646137"
62
- },
63
- "offering_hash_input_hex": "31333a617a7572655f666f756e64727931313a70726f643a65617374757331333a6465706c6f793a6770742d346f",
64
- "expected_offering_id": "off:v1:7dd14f98ad4bfec51ffb169ea20dc17aed1ebe03a3c9fdfce89f868454c05da7",
65
- "lane_hash_input_hex": "6c616e652d76310031333a617a7572655f666f756e64727931313a70726f643a656173747573343a63686174363a6770742d346f37313a6f66663a76313a37646431346639386164346266656335316666623136396561323064633137616564316562653033613363396664666365383966383638343534633035646137",
66
- "expected_lane_id": "lane:v1:2334bc87506ae6312a4f64b53798d723d07279294ee6a3181e23db298cf910aa"
67
- }
68
- ],
69
- "tier_invariance": {
70
- "note": "The vllm+h200+gemma4+chat vector reproduces identical offering_id/lane_id under every tier; tier is not framed.",
71
- "base_vector_index": 0,
72
- "tiers": [
73
- "local",
74
- "frontier"
75
- ]
76
- },
77
- "independence": {
78
- "note": "vectors[0] and vectors[1] share provider_family+model but differ by instance_id, proving vllm+h200+gemma4 and vllm+helios1+gemma4 are independent identities.",
79
- "vector_indexes": [
80
- 0,
81
- 1
82
- ]
83
- }
84
- }
@@ -1,127 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'spec_helper'
4
-
5
- RSpec.describe Legion::Extensions::Llm::Context do
6
- include_context 'with configured Legion::Extensions::Llm'
7
- include_context 'with fake llm provider'
8
-
9
- describe '#initialize' do
10
- it 'creates a copy of the global configuration' do
11
- # Get current config values
12
- original_model = Legion::Extensions::Llm.config.default_model
13
- original_log_regexp_timeout = Legion::Extensions::Llm.config.log_regexp_timeout
14
-
15
- # Create context with modified config
16
- context = Legion::Extensions::Llm.context do |config|
17
- config.default_model = 'modified-model'
18
- config.fake_llm_api_key = 'modified-key'
19
- config.log_regexp_timeout = 5.0
20
- end
21
-
22
- # Verify global config is unchanged
23
- expect(Legion::Extensions::Llm.config.default_model).to eq(original_model)
24
- expect(Legion::Extensions::Llm.config.log_regexp_timeout).to eq(original_log_regexp_timeout)
25
-
26
- # Verify context has modified config
27
- expect(context.config.default_model).to eq('modified-model')
28
- expect(context.config.fake_llm_api_key).to eq('modified-key')
29
- expect(context.config.log_regexp_timeout).to eq(5.0)
30
- end
31
-
32
- it 'preserves log_regexp_timeout when Regexp timeout is unavailable' do
33
- allow(Regexp).to receive(:respond_to?).and_call_original
34
- allow(Regexp).to receive(:respond_to?).with(:timeout).and_return(false)
35
- allow(Legion::Extensions::Llm.logger).to receive(:warn)
36
-
37
- context = Legion::Extensions::Llm.context do |config|
38
- config.log_regexp_timeout = 5.0
39
- end
40
-
41
- expect(context.config.log_regexp_timeout).to eq(5.0)
42
- end
43
- end
44
-
45
- describe 'context chat operations' do
46
- it 'creates a chat with context-specific configuration' do
47
- context = Legion::Extensions::Llm.context do |config|
48
- config.default_model = 'fake-chat-model'
49
- end
50
-
51
- chat = context.chat(provider: :fake_llm, assume_model_exists: true)
52
- expect(chat.model.id).to eq('fake-chat-model')
53
- end
54
-
55
- it 'allows specifying a model when creating the chat' do
56
- context = Legion::Extensions::Llm.context do |config|
57
- config.default_model = 'fake-chat-model'
58
- end
59
-
60
- chat = context.chat(model: 'other-fake-chat-model', provider: :fake_llm, assume_model_exists: true)
61
- expect(chat.model.id).to eq('other-fake-chat-model')
62
- end
63
- end
64
-
65
- describe 'context embed operations' do
66
- it 'respects context-specific embedding model' do
67
- context = Legion::Extensions::Llm.context do |config|
68
- config.default_embedding_model = 'fake-embed'
69
- end
70
-
71
- embedding = context.embed('Test embedding', provider: :fake_llm, assume_model_exists: true)
72
- expect(embedding.model).to eq('fake-embed')
73
- end
74
-
75
- it 'allows specifying a model at embed time' do
76
- context = Legion::Extensions::Llm.context do |config|
77
- config.default_embedding_model = 'fake-embed'
78
- end
79
-
80
- embedding = context.embed('Test embedding', model: 'override-embed', provider: :fake_llm,
81
- assume_model_exists: true)
82
- expect(embedding.model).to eq('override-embed')
83
- end
84
- end
85
-
86
- describe 'multiple independent contexts' do
87
- it 'allows multiple contexts with different configurations' do
88
- context1 = Legion::Extensions::Llm.context do |config|
89
- config.default_model = 'fake-chat-1'
90
- config.log_regexp_timeout = 5.0
91
- end
92
-
93
- context2 = Legion::Extensions::Llm.context do |config|
94
- config.default_model = 'fake-chat-2'
95
- end
96
-
97
- chat1 = context1.chat(provider: :fake_llm, assume_model_exists: true)
98
- chat2 = context2.chat(provider: :fake_llm, assume_model_exists: true)
99
-
100
- expect(chat1.model.id).to eq('fake-chat-1')
101
- expect(context1.config.log_regexp_timeout).to eq(5.0)
102
-
103
- expect(chat2.model.id).to eq('fake-chat-2')
104
- expected_timeout = Regexp.respond_to?(:timeout) ? (Regexp.timeout || 1.0) : nil
105
- expect(context2.config.log_regexp_timeout).to eq(expected_timeout)
106
- end
107
-
108
- it 'ensures changes in one context do not affect another' do
109
- context1 = Legion::Extensions::Llm.context do |config|
110
- config.fake_llm_api_key = 'key1'
111
- config.default_model = 'model1'
112
- end
113
-
114
- context2 = Legion::Extensions::Llm.context do |config|
115
- config.fake_llm_api_key = 'key2'
116
- config.default_model = 'model2'
117
- end
118
-
119
- # Modify context1 after creation
120
- context1.config.fake_llm_api_key = 'modified-key1'
121
-
122
- # Context2 should be unaffected
123
- expect(context2.config.fake_llm_api_key).to eq('key2')
124
- expect(context2.config.default_model).to eq('model2')
125
- end
126
- end
127
- end
@@ -1,43 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'spec_helper'
4
-
5
- RSpec.describe Legion::Extensions::Llm::Inventory::Capabilities do
6
- describe '.normalize' do
7
- it 'returns symbols for raw capability names' do
8
- expect(described_class.normalize(%i[tools streaming])).to contain_exactly(:tools, :streaming)
9
- end
10
-
11
- it 'collapses provider aliases into the canonical vocabulary' do
12
- expect(described_class.normalize([:function_calling])).to include(:tools)
13
- expect(described_class.normalize([:tool_use])).to include(:tools)
14
- expect(described_class.normalize([:stream])).to include(:streaming)
15
- end
16
-
17
- it 'returns an empty array for nil/empty input' do
18
- expect(described_class.normalize(nil)).to eq([])
19
- expect(described_class.normalize([])).to eq([])
20
- end
21
-
22
- it 'is the same vocabulary as Legion::Extensions::Llm::Capabilities' do
23
- expect(described_class::ALIASES).to equal(Legion::Extensions::Llm::Capabilities::ALIASES)
24
- end
25
- end
26
-
27
- describe '.merge' do
28
- it 'unions multiple capability sets' do
29
- result = described_class.merge([:tools], %i[streaming tools])
30
- expect(result).to contain_exactly(:tools, :streaming)
31
- end
32
- end
33
-
34
- describe '.include_all?' do
35
- it 'returns true when required is a subset of available' do
36
- expect(described_class.include_all?(%i[tools streaming], [:tools])).to be(true)
37
- end
38
-
39
- it 'returns false when required is missing from available' do
40
- expect(described_class.include_all?([:streaming], [:tools])).to be(false)
41
- end
42
- end
43
- end