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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +106 -0
- data/RULES.md +97 -0
- data/lib/legion/extensions/llm/auto_registration.rb +4 -13
- data/lib/legion/extensions/llm/canonical/chunk.rb +106 -92
- data/lib/legion/extensions/llm/canonical/content_block.rb +66 -75
- data/lib/legion/extensions/llm/canonical/message.rb +52 -67
- data/lib/legion/extensions/llm/canonical/params.rb +59 -33
- data/lib/legion/extensions/llm/canonical/request.rb +67 -54
- data/lib/legion/extensions/llm/canonical/response.rb +61 -76
- data/lib/legion/extensions/llm/canonical/strict.rb +105 -0
- data/lib/legion/extensions/llm/canonical/thinking.rb +99 -51
- data/lib/legion/extensions/llm/canonical/tool_call.rb +42 -51
- data/lib/legion/extensions/llm/canonical/tool_definition.rb +56 -46
- data/lib/legion/extensions/llm/canonical/tool_schema.rb +15 -22
- data/lib/legion/extensions/llm/canonical/usage.rb +47 -40
- data/lib/legion/extensions/llm/canonical.rb +6 -5
- data/lib/legion/extensions/llm/configuration.rb +40 -10
- data/lib/legion/extensions/llm/connection.rb +8 -30
- data/lib/legion/extensions/llm/credential_sources.rb +32 -49
- data/lib/legion/extensions/llm/discovery/actor.rb +92 -0
- data/lib/legion/extensions/llm/discovery/pipeline.rb +604 -0
- data/lib/legion/extensions/llm/error.rb +0 -14
- data/lib/legion/extensions/llm/fleet/contract_error.rb +15 -0
- data/lib/legion/extensions/llm/fleet/envelope_validation.rb +7 -6
- data/lib/legion/extensions/llm/fleet/fleet_envelope.rb +66 -0
- data/lib/legion/extensions/llm/fleet/protocol.rb +18 -5
- data/lib/legion/extensions/llm/fleet/provider_responder.rb +58 -153
- data/lib/legion/extensions/llm/fleet/token_validator.rb +15 -21
- data/lib/legion/extensions/llm/fleet/worker_execution.rb +107 -150
- data/lib/legion/extensions/llm/inventory/errors.rb +0 -1
- data/lib/legion/extensions/llm/inventory/evidence.rb +1 -1
- data/lib/legion/extensions/llm/inventory/identity.rb +55 -39
- data/lib/legion/extensions/llm/inventory/probe_token.rb +9 -12
- data/lib/legion/extensions/llm/inventory/publisher.rb +15 -53
- data/lib/legion/extensions/llm/inventory/records.rb +82 -99
- data/lib/legion/extensions/llm/inventory/registry.rb +54 -46
- data/lib/legion/extensions/llm/inventory/snapshot.rb +7 -21
- data/lib/legion/extensions/llm/inventory/weight_reconciler.rb +249 -0
- data/lib/legion/extensions/llm/inventory/weight_schema.rb +146 -0
- data/lib/legion/extensions/llm/provider/open_ai_compatible.rb +172 -182
- data/lib/legion/extensions/llm/provider.rb +192 -315
- data/lib/legion/extensions/llm/provider_contract.rb +25 -8
- data/lib/legion/extensions/llm/provider_settings.rb +5 -26
- data/lib/legion/extensions/llm/responses/thinking_extractor.rb +8 -1
- data/lib/legion/extensions/llm/responses/tool_arguments.rb +34 -0
- data/lib/legion/extensions/llm/routing/provider_outcome.rb +25 -0
- data/lib/legion/extensions/llm/routing/records.rb +34 -17
- data/lib/legion/extensions/llm/stream_accumulator.rb +186 -270
- data/lib/legion/extensions/llm/streaming.rb +50 -35
- data/lib/legion/extensions/llm/taxonomies.rb +29 -17
- data/lib/legion/extensions/llm/transport/fleet_lane.rb +8 -10
- data/lib/legion/extensions/llm/transport/messages/fleet_error.rb +3 -2
- data/lib/legion/extensions/llm/transport/messages/fleet_request.rb +6 -8
- data/lib/legion/extensions/llm/transport/messages/fleet_response.rb +10 -9
- data/lib/legion/extensions/llm/utils.rb +23 -5
- data/lib/legion/extensions/llm/version.rb +1 -1
- data/lib/legion/extensions/llm.rb +10 -98
- data/spec/legion/extensions/llm/auto_registration_spec.rb +4 -9
- data/spec/legion/extensions/llm/canonical/chunk_spec.rb +66 -252
- data/spec/legion/extensions/llm/canonical/content_block_spec.rb +52 -197
- data/spec/legion/extensions/llm/canonical/message_spec.rb +89 -204
- data/spec/legion/extensions/llm/canonical/params_spec.rb +57 -136
- data/spec/legion/extensions/llm/canonical/request_spec.rb +81 -143
- data/spec/legion/extensions/llm/canonical/response_spec.rb +68 -204
- data/spec/legion/extensions/llm/canonical/thinking_spec.rb +68 -148
- data/spec/legion/extensions/llm/canonical/tool_call_spec.rb +59 -162
- data/spec/legion/extensions/llm/canonical/tool_definition_spec.rb +55 -191
- data/spec/legion/extensions/llm/canonical/tool_schema_spec.rb +26 -67
- data/spec/legion/extensions/llm/canonical/usage_spec.rb +46 -155
- data/spec/legion/extensions/llm/configuration_spec.rb +31 -5
- data/spec/legion/extensions/llm/conformance/canonical_type_examples.rb +106 -0
- data/spec/legion/extensions/llm/conformance/conformance.rb +10 -2
- data/spec/legion/extensions/llm/conformance/provider_translator_examples.rb +1 -1
- data/spec/legion/extensions/llm/conformance/ssot_contract_conformance_spec.rb +130 -0
- data/spec/legion/extensions/llm/conformance/ssot_contract_examples.rb +507 -0
- data/spec/legion/extensions/llm/conformance/ssot_provider_examples.rb +11 -10
- data/spec/legion/extensions/llm/credential_sources_spec.rb +12 -13
- data/spec/legion/extensions/llm/error_spec.rb +2 -12
- data/spec/legion/extensions/llm/fleet/exact_offering_spec.rb +59 -45
- data/spec/legion/extensions/llm/fleet/provider_responder_spec.rb +178 -67
- data/spec/legion/extensions/llm/fleet/token_validator_spec.rb +7 -2
- data/spec/legion/extensions/llm/fleet/worker_execution_spec.rb +101 -62
- data/spec/legion/extensions/llm/fleet_messages_spec.rb +118 -123
- data/spec/legion/extensions/llm/inventory/boot_spec.rb +4 -4
- data/spec/legion/extensions/llm/inventory/identity_spec.rb +127 -110
- data/spec/legion/extensions/llm/inventory/probe_token_spec.rb +4 -4
- data/spec/legion/extensions/llm/inventory/publisher_spec.rb +8 -61
- data/spec/legion/extensions/llm/inventory/records_spec.rb +129 -41
- data/spec/legion/extensions/llm/inventory/registry_activation_spec.rb +55 -4
- data/spec/legion/extensions/llm/inventory/registry_replacement_spec.rb +5 -4
- data/spec/legion/extensions/llm/inventory/snapshot_spec.rb +11 -5
- data/spec/legion/extensions/llm/inventory/weight_reconciler_spec.rb +312 -0
- data/spec/legion/extensions/llm/inventory/weight_schema_spec.rb +141 -0
- data/spec/legion/extensions/llm/provider/open_ai_compatible_spec.rb +105 -68
- data/spec/legion/extensions/llm/provider/open_ai_compatible_tool_calls_array_spec.rb +7 -31
- data/spec/legion/extensions/llm/provider_contract_spec.rb +10 -15
- data/spec/legion/extensions/llm/provider_spec.rb +98 -78
- data/spec/legion/extensions/llm/routing/records_spec.rb +38 -5
- data/spec/legion/extensions/llm/stream_accumulator_spec.rb +174 -144
- data/spec/legion/extensions/llm/streaming_spec.rb +27 -0
- data/spec/legion/extensions/llm/taxonomies_spec.rb +43 -16
- data/spec/legion/extensions/llm/transport/fleet_lane_spec.rb +1 -1
- data/spec/legion/extensions/llm/utils_spec.rb +26 -7
- data/spec/legion/extensions/llm_base_contract_spec.rb +55 -90
- data/spec/legion/extensions/llm_extension_spec.rb +5 -5
- data/spec/support/fake_llm_provider.rb +45 -39
- data/spec/support/fake_ssot_harness.rb +7 -2
- data/spec/support/ssot_registry_helpers.rb +3 -2
- metadata +15 -54
- data/lib/legion/extensions/llm/agent.rb +0 -366
- data/lib/legion/extensions/llm/aliases.json +0 -436
- data/lib/legion/extensions/llm/aliases.rb +0 -67
- data/lib/legion/extensions/llm/attachment.rb +0 -229
- data/lib/legion/extensions/llm/chat.rb +0 -354
- data/lib/legion/extensions/llm/chunk.rb +0 -10
- data/lib/legion/extensions/llm/content.rb +0 -81
- data/lib/legion/extensions/llm/context.rb +0 -33
- data/lib/legion/extensions/llm/embedding.rb +0 -33
- data/lib/legion/extensions/llm/image.rb +0 -109
- data/lib/legion/extensions/llm/inventory/capabilities.rb +0 -40
- data/lib/legion/extensions/llm/inventory/scoped_refresher.rb +0 -311
- data/lib/legion/extensions/llm/message.rb +0 -118
- data/lib/legion/extensions/llm/mime_type.rb +0 -75
- data/lib/legion/extensions/llm/model/info.rb +0 -286
- data/lib/legion/extensions/llm/model/modalities.rb +0 -26
- data/lib/legion/extensions/llm/model/pricing.rb +0 -52
- data/lib/legion/extensions/llm/model/pricing_category.rb +0 -50
- data/lib/legion/extensions/llm/model/pricing_tier.rb +0 -37
- data/lib/legion/extensions/llm/model.rb +0 -11
- data/lib/legion/extensions/llm/models.json +0 -57313
- data/lib/legion/extensions/llm/models.rb +0 -530
- data/lib/legion/extensions/llm/models_schema.json +0 -168
- data/lib/legion/extensions/llm/moderation.rb +0 -60
- data/lib/legion/extensions/llm/registry_event_builder.rb +0 -141
- data/lib/legion/extensions/llm/registry_publisher.rb +0 -107
- data/lib/legion/extensions/llm/responses/chat_response.rb +0 -43
- data/lib/legion/extensions/llm/responses/embedding_response.rb +0 -38
- data/lib/legion/extensions/llm/responses/stream_chunk.rb +0 -43
- data/lib/legion/extensions/llm/routing/lane_key.rb +0 -66
- data/lib/legion/extensions/llm/routing/model_offering.rb +0 -241
- data/lib/legion/extensions/llm/routing/offering_registry.rb +0 -101
- data/lib/legion/extensions/llm/routing/registry_event.rb +0 -167
- data/lib/legion/extensions/llm/thinking.rb +0 -53
- data/lib/legion/extensions/llm/tokens.rb +0 -51
- data/lib/legion/extensions/llm/tool_call.rb +0 -34
- data/lib/legion/extensions/llm/transcription.rb +0 -39
- data/lib/legion/extensions/llm/transport/messages/registry_event.rb +0 -44
- data/spec/legion/extensions/llm/agent_spec.rb +0 -179
- data/spec/legion/extensions/llm/attachment_spec.rb +0 -25
- data/spec/legion/extensions/llm/conformance/fixtures/ssot_identity_vectors.json +0 -84
- data/spec/legion/extensions/llm/context_spec.rb +0 -127
- data/spec/legion/extensions/llm/inventory/capabilities_spec.rb +0 -43
- data/spec/legion/extensions/llm/inventory/scoped_refresher_spec.rb +0 -337
- data/spec/legion/extensions/llm/message_spec.rb +0 -64
- data/spec/legion/extensions/llm/model/info_spec.rb +0 -222
- data/spec/legion/extensions/llm/models_spec.rb +0 -104
- data/spec/legion/extensions/llm/registry_event_builder_spec.rb +0 -68
- data/spec/legion/extensions/llm/registry_publisher_spec.rb +0 -22
- data/spec/legion/extensions/llm/responses/response_objects_spec.rb +0 -75
- data/spec/legion/extensions/llm/routing/model_offering_spec.rb +0 -281
- data/spec/legion/extensions/llm/routing/offering_registry_spec.rb +0 -50
- data/spec/legion/extensions/llm/routing/registry_event_spec.rb +0 -120
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'spec_helper'
|
|
4
|
+
require 'support/ssot_registry_helpers'
|
|
4
5
|
|
|
5
6
|
RSpec.describe Legion::Extensions::Llm::Provider do
|
|
7
|
+
include SsotRegistryHelpers
|
|
8
|
+
|
|
6
9
|
describe 'Hash config support' do
|
|
7
10
|
let(:provider_class) do
|
|
8
11
|
Class.new(described_class) do
|
|
@@ -100,38 +103,19 @@ RSpec.describe Legion::Extensions::Llm::Provider do
|
|
|
100
103
|
end
|
|
101
104
|
|
|
102
105
|
describe 'canonical provider contract' do
|
|
103
|
-
let(:model) do
|
|
104
|
-
Legion::Extensions::Llm::Model::Info.new(
|
|
105
|
-
id: 'test-model',
|
|
106
|
-
provider: :contract,
|
|
107
|
-
instance: :primary,
|
|
108
|
-
capabilities: %i[completion streaming tools],
|
|
109
|
-
context_length: 8192,
|
|
110
|
-
metadata: { max_output_tokens: 2048 }
|
|
111
|
-
)
|
|
112
|
-
end
|
|
113
|
-
|
|
114
106
|
let(:provider_class) do
|
|
115
|
-
model_info = model
|
|
116
107
|
Class.new(described_class) do
|
|
117
108
|
def self.name = 'Provider'
|
|
118
109
|
|
|
119
110
|
define_method(:api_base) { 'https://contract.invalid' }
|
|
120
111
|
define_method(:models_url) { '/v1/models' }
|
|
121
|
-
attr_reader :list_model_calls
|
|
122
|
-
|
|
123
|
-
define_method(:list_models) do |live: false, **filters|
|
|
124
|
-
@list_model_calls ||= []
|
|
125
|
-
@list_model_calls << { live: live, filters: filters }
|
|
126
|
-
[model_info]
|
|
127
|
-
end
|
|
128
112
|
|
|
129
113
|
def render_payload(_messages, **)
|
|
130
114
|
{}
|
|
131
115
|
end
|
|
132
116
|
|
|
133
117
|
def parse_completion_response(_response)
|
|
134
|
-
Legion::Extensions::Llm::
|
|
118
|
+
Legion::Extensions::Llm::Canonical::Response.build(text: 'ok')
|
|
135
119
|
end
|
|
136
120
|
end
|
|
137
121
|
end
|
|
@@ -143,12 +127,12 @@ RSpec.describe Legion::Extensions::Llm::Provider do
|
|
|
143
127
|
instance_id: :primary })
|
|
144
128
|
end
|
|
145
129
|
|
|
146
|
-
it 'exposes a canonical chat alias over complete' do
|
|
130
|
+
it 'exposes a canonical chat alias over complete (0.8.0 signature — O4 temperature in Params)' do
|
|
147
131
|
allow(provider).to receive(:complete).and_return('ok')
|
|
148
132
|
|
|
149
|
-
expect(provider.chat(
|
|
133
|
+
expect(provider.chat([], model: 'test-model')).to eq('ok')
|
|
150
134
|
expect(provider).to have_received(:complete).with(
|
|
151
|
-
[], tools: [],
|
|
135
|
+
[], tools: [], model: 'test-model', params: nil, headers: {},
|
|
152
136
|
schema: nil, thinking: nil, tool_prefs: nil
|
|
153
137
|
)
|
|
154
138
|
end
|
|
@@ -157,73 +141,86 @@ RSpec.describe Legion::Extensions::Llm::Provider do
|
|
|
157
141
|
seen = []
|
|
158
142
|
allow(provider).to receive(:complete) { |_messages, **_opts, &block| block.call('chunk') }
|
|
159
143
|
|
|
160
|
-
provider.stream_chat(
|
|
144
|
+
provider.stream_chat([], model: 'test-model') { |chunk| seen << chunk }
|
|
161
145
|
|
|
162
146
|
expect(seen).to eq(['chunk'])
|
|
163
147
|
end
|
|
164
148
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
expect(offering.provider_instance).to eq(:primary)
|
|
172
|
-
expect(offering.model).to eq('test-model')
|
|
173
|
-
expect(offering.usage_type).to eq(:inference)
|
|
174
|
-
expect(offering.capabilities).to include(:completion, :streaming, :tools)
|
|
175
|
-
expect(offering.context_window).to eq(8192)
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
it 'publishes every discovered model before policy filtering removes blocked models' do
|
|
179
|
-
blocked_model = Legion::Extensions::Llm::Model::Info.new(
|
|
180
|
-
id: 'blocked-model',
|
|
181
|
-
provider: :contract,
|
|
182
|
-
instance: :primary,
|
|
183
|
-
capabilities: %i[completion],
|
|
184
|
-
context_length: 4096
|
|
149
|
+
# 0.8.0 (07 C5): the legacy ModelOffering production path is deleted — the
|
|
150
|
+
# per-gem writer publishes; the base read path serves the activated
|
|
151
|
+
# inventory offerings for this instance from the registry snapshot.
|
|
152
|
+
def activate_contract_instance
|
|
153
|
+
key = Legion::Extensions::Llm::Inventory::Identity::InstanceKey.new(
|
|
154
|
+
provider_family: 'provider', instance_id: 'primary'
|
|
185
155
|
)
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
allow(provider_class).to receive(:registry_publisher).and_return(registry_publisher)
|
|
189
|
-
allow(provider).to receive_messages(list_models: [model, blocked_model], settings: { model_blacklist: ['blocked'] })
|
|
190
|
-
|
|
191
|
-
offerings = provider.discover_offerings(live: true)
|
|
192
|
-
|
|
193
|
-
expect(offerings.map(&:model)).to eq(['test-model'])
|
|
194
|
-
expect(registry_publisher).to have_received(:publish_models_async).with([model], anything)
|
|
195
|
-
expect(registry_publisher).to have_received(:publish_models_async).with([blocked_model], anything)
|
|
156
|
+
Legion::Extensions::Llm::Inventory::Registry.reset!
|
|
157
|
+
claim_and_activate(key: key, callable: Object.new, coordinator: probe_coordinator(key), model: 'test-model')
|
|
196
158
|
end
|
|
197
159
|
|
|
198
|
-
it '
|
|
199
|
-
|
|
160
|
+
it 'serves the activated inventory offerings for the instance from the registry snapshot (07 C5)' do
|
|
161
|
+
activate_contract_instance
|
|
162
|
+
offerings = provider.discover_offerings
|
|
200
163
|
|
|
201
|
-
expect(
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
)
|
|
164
|
+
expect(offerings.size).to eq(1)
|
|
165
|
+
expect(offerings.first.model).to eq('test-model')
|
|
166
|
+
expect(offerings.first.instance_key.instance_id).to eq('primary')
|
|
167
|
+
expect(offerings.first.instance_key.provider_family).to eq(:provider)
|
|
205
168
|
end
|
|
206
169
|
|
|
207
|
-
it 'filters
|
|
208
|
-
|
|
170
|
+
it 'filters snapshot offerings by model and instance keys' do
|
|
171
|
+
activate_contract_instance
|
|
209
172
|
|
|
210
|
-
expect(provider.discover_offerings(
|
|
211
|
-
expect(provider.discover_offerings(
|
|
212
|
-
expect(provider.discover_offerings(
|
|
173
|
+
expect(provider.discover_offerings(model: 'test-model')).not_to be_empty
|
|
174
|
+
expect(provider.discover_offerings(id: 'test-model')).not_to be_empty
|
|
175
|
+
expect(provider.discover_offerings(model: 'other-model')).to be_empty
|
|
176
|
+
expect(provider.discover_offerings(instance: 'other')).to be_empty
|
|
213
177
|
end
|
|
214
178
|
|
|
215
|
-
it '
|
|
179
|
+
it 'never calls list_models — the production path moved to the per-gem writer' do
|
|
216
180
|
allow(provider).to receive(:list_models).and_raise('unexpected live discovery')
|
|
217
181
|
|
|
218
|
-
expect
|
|
182
|
+
expect { provider.discover_offerings(live: true) }.not_to raise_error
|
|
219
183
|
expect(provider).not_to have_received(:list_models)
|
|
220
184
|
end
|
|
221
185
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
186
|
+
describe 'H5 — honest read-path contract (accepted-for-compat params are documented, not silently discarded)' do
|
|
187
|
+
it 'accepts the full discover_offerings signature against the snapshot' do
|
|
188
|
+
expect { provider.discover_offerings(live: true, raise_on_unreachable: true) }.not_to raise_error
|
|
189
|
+
expect(provider.discover_offerings(raise_on_unreachable: true, model: 'test-model').size).to eq(1)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
describe 'H3 — the funnel enforces the tool contract (one contract, one oracle)' do
|
|
194
|
+
let(:tool) { Legion::Extensions::Llm::Canonical::ToolDefinition.build(name: 'get_weather') }
|
|
195
|
+
|
|
196
|
+
before do
|
|
197
|
+
allow(provider).to receive(:sync_response).and_return('ok') # -- stream_response's model: kwarg is not exercised here
|
|
198
|
+
allow(provider).to receive(:stream_response) { |_connection, _payload, _headers, **_opts, &stream_block| stream_block&.call('c') }
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
it 'accepts Hash<name, Canonical::ToolDefinition> through chat and stream_chat' do
|
|
202
|
+
expect(provider.chat([], model: 'test-model', tools: { get_weather: tool })).to eq('ok')
|
|
203
|
+
expect do
|
|
204
|
+
# rubocop:disable Lint/EmptyBlock -- the streamed chunk is consumed by the stub
|
|
205
|
+
provider.stream_chat([], model: 'test-model', tools: { get_weather: tool }) { |_c| }
|
|
206
|
+
# rubocop:enable Lint/EmptyBlock
|
|
207
|
+
end.not_to raise_error
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
it 'rejects Hash tool values with a typed ArgumentError' do
|
|
211
|
+
expect { provider.chat([], model: 'test-model', tools: { get_weather: { name: 'get_weather' } }) }
|
|
212
|
+
.to raise_error(ArgumentError, /Canonical::ToolDefinition/)
|
|
213
|
+
end
|
|
225
214
|
|
|
226
|
-
|
|
215
|
+
it 'rejects a non-Hash tools container with a typed ArgumentError' do
|
|
216
|
+
expect { provider.chat([], model: 'test-model', tools: [tool]) }
|
|
217
|
+
.to raise_error(ArgumentError, /Hash<name, Canonical::ToolDefinition>/)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
it 'rejects legacy tool objects with a typed ArgumentError' do
|
|
221
|
+
expect { provider.chat([], model: 'test-model', tools: { get_weather: Legion::Extensions::Llm::Tool.new }) }
|
|
222
|
+
.to raise_error(ArgumentError, /Canonical::ToolDefinition/)
|
|
223
|
+
end
|
|
227
224
|
end
|
|
228
225
|
|
|
229
226
|
it 'returns normalized health metadata' do
|
|
@@ -237,16 +234,17 @@ RSpec.describe Legion::Extensions::Llm::Provider do
|
|
|
237
234
|
end
|
|
238
235
|
|
|
239
236
|
it 'provides a deterministic token estimate fallback' do
|
|
240
|
-
|
|
237
|
+
messages = [Legion::Extensions::Llm::Canonical::Message.build(role: :user, content: 'hello world')]
|
|
238
|
+
expect(provider.count_tokens(messages: messages, model: 'test-model')).to be >= 1
|
|
241
239
|
end
|
|
242
240
|
|
|
243
|
-
it 'summarizes
|
|
241
|
+
it 'summarizes canonical tools for debug logging' do
|
|
244
242
|
tools = {
|
|
245
|
-
current:
|
|
246
|
-
|
|
243
|
+
current: Legion::Extensions::Llm::Canonical::ToolDefinition.build(name: 'current'),
|
|
244
|
+
other: Legion::Extensions::Llm::Canonical::ToolDefinition.build(name: 'other')
|
|
247
245
|
}
|
|
248
246
|
|
|
249
|
-
expect(provider.send(:debug_tool_names, tools)).to eq(%w[current
|
|
247
|
+
expect(provider.send(:debug_tool_names, tools)).to eq(%w[current other])
|
|
250
248
|
end
|
|
251
249
|
|
|
252
250
|
it 'deep merges embedding params into the provider payload' do
|
|
@@ -375,6 +373,28 @@ RSpec.describe Legion::Extensions::Llm::Provider do
|
|
|
375
373
|
expect(provider.model_allowed?('gpt-5')).to be true
|
|
376
374
|
end
|
|
377
375
|
end
|
|
376
|
+
|
|
377
|
+
context 'when the model is an object exposing #id' do
|
|
378
|
+
let(:model_info) do
|
|
379
|
+
Struct.new(:id).new('claude-sonnet-4-6')
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
before { provider.settings = { model_whitelist: %w[sonnet] } }
|
|
383
|
+
|
|
384
|
+
it 'matches policy against the model id, not the inspect string' do
|
|
385
|
+
expect(provider.model_allowed?(model_info)).to be true
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
it 'blocks object models the whitelist does not cover' do
|
|
389
|
+
other = Struct.new(:id).new('llama-3')
|
|
390
|
+
expect(provider.model_allowed?(other)).to be false
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
it 'never matches inspect-string artifacts (e.g. a data/name pattern)' do
|
|
394
|
+
provider.settings = { model_whitelist: %w[data] }
|
|
395
|
+
expect(provider.model_allowed?(model_info)).to be false
|
|
396
|
+
end
|
|
397
|
+
end
|
|
378
398
|
end
|
|
379
399
|
|
|
380
400
|
describe '#enforce_model_allowed! (dispatch compliance guard)' do
|
|
@@ -407,7 +427,7 @@ RSpec.describe Legion::Extensions::Llm::Provider do
|
|
|
407
427
|
end
|
|
408
428
|
|
|
409
429
|
it 'fails closed in #complete before any provider call' do
|
|
410
|
-
expect { provider.complete([], tools: [],
|
|
430
|
+
expect { provider.complete([], tools: [], model: 'gpt-5') }
|
|
411
431
|
.to raise_error(Legion::Extensions::Llm::ModelNotAllowedError)
|
|
412
432
|
end
|
|
413
433
|
|
|
@@ -421,7 +441,7 @@ RSpec.describe Legion::Extensions::Llm::Provider do
|
|
|
421
441
|
before { provider.settings = { model_blacklist: %w[sonnet] } }
|
|
422
442
|
|
|
423
443
|
it 'fails closed in #complete for a blacklisted model' do
|
|
424
|
-
expect { provider.complete([], tools: [],
|
|
444
|
+
expect { provider.complete([], tools: [], model: 'claude-sonnet-4-6') }
|
|
425
445
|
.to raise_error(Legion::Extensions::Llm::ModelNotAllowedError)
|
|
426
446
|
end
|
|
427
447
|
end
|
|
@@ -16,9 +16,11 @@ RSpec.describe Legion::Extensions::Llm::Routing::Records do
|
|
|
16
16
|
end
|
|
17
17
|
let(:callable_handle) { inventory::CallableHandle.new(handle_id: 'call:v1:x', callable: callable_class.new) }
|
|
18
18
|
let(:instance_key) { identity::InstanceKey.new(provider_family: 'vllm', instance_id: 'h200') }
|
|
19
|
-
let(:offering_id) { identity.offering_id(instance_key: instance_key, provider_native_key: 'gemma4') }
|
|
20
19
|
let(:lane_id) do
|
|
21
|
-
identity.
|
|
20
|
+
identity.compose_lane_id(
|
|
21
|
+
tier: :local, provider_family: 'vllm', instance_id: 'h200',
|
|
22
|
+
type: Legion::Extensions::Llm::Taxonomies.lane_type_for(operation: :chat), model: 'gemma4'
|
|
23
|
+
)
|
|
22
24
|
end
|
|
23
25
|
|
|
24
26
|
describe routing::AttemptTargetKey do
|
|
@@ -76,7 +78,7 @@ RSpec.describe Legion::Extensions::Llm::Routing::Records do
|
|
|
76
78
|
describe routing::Selection do
|
|
77
79
|
def build_selection(**overrides)
|
|
78
80
|
Legion::Extensions::Llm::Routing::Selection.new(
|
|
79
|
-
inventory_generation: 7, lane_id: lane_id, instance_key: instance_key,
|
|
81
|
+
inventory_generation: 7, lane_id: lane_id, instance_key: instance_key,
|
|
80
82
|
provider_family: :vllm, instance_id: 'h200', model: 'gemma4', operation: :chat,
|
|
81
83
|
callable_handle: callable_handle, publisher_token_id: 'ptok:v1:abc', capability_evidence: {},
|
|
82
84
|
context_evidence: Legion::Extensions::Llm::Inventory::ValueEvidence.new(status: :unknown, source: :absent),
|
|
@@ -88,6 +90,8 @@ RSpec.describe Legion::Extensions::Llm::Routing::Records do
|
|
|
88
90
|
|
|
89
91
|
it 'builds a valid selection and derives the attempt target key' do
|
|
90
92
|
selection = build_selection
|
|
93
|
+
expect(selection.lane_id).to eq('local:vllm:h200:inference:gemma4')
|
|
94
|
+
expect(selection.members).not_to include(:offering_id)
|
|
91
95
|
key = selection.attempt_target_key
|
|
92
96
|
expect(key).to eq(routing::AttemptTargetKey.new(provider_family: :vllm, instance_id: 'h200', model: 'gemma4'))
|
|
93
97
|
end
|
|
@@ -105,8 +109,37 @@ RSpec.describe Legion::Extensions::Llm::Routing::Records do
|
|
|
105
109
|
.to raise_error(errors::ValidationError)
|
|
106
110
|
end
|
|
107
111
|
|
|
108
|
-
it 'rejects a lane_id that
|
|
109
|
-
expect { build_selection(lane_id:
|
|
112
|
+
it 'rejects a lane_id that is not a 5-tuple' do
|
|
113
|
+
expect { build_selection(lane_id: 'not-a-five-tuple') }
|
|
114
|
+
.to raise_error(errors::ValidationError)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it 'rejects a lane_id whose family part disagrees with the selection' do
|
|
118
|
+
expect { build_selection(lane_id: 'local:openai:h200:inference:gemma4') }
|
|
119
|
+
.to raise_error(errors::ValidationError, /provider_family/)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it 'rejects a lane_id whose instance part disagrees with the selection' do
|
|
123
|
+
expect { build_selection(lane_id: 'local:vllm:helios1:inference:gemma4') }
|
|
124
|
+
.to raise_error(errors::ValidationError, /instance_id/)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it 'rejects a lane_id whose type part disagrees with the operation' do
|
|
128
|
+
expect { build_selection(lane_id: 'local:vllm:h200:embedding:gemma4') }
|
|
129
|
+
.to raise_error(errors::ValidationError, /type/)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it 'rejects a lane_id whose model part disagrees with the selection' do
|
|
133
|
+
expect { build_selection(lane_id: 'local:vllm:h200:inference:other-model') }
|
|
134
|
+
.to raise_error(errors::ValidationError, /model/)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it 'keeps colons inside the lane id model part' do
|
|
138
|
+
colon_model = 'llama3.2:8b-instruct'
|
|
139
|
+
id = identity.compose_lane_id(
|
|
140
|
+
tier: :local, provider_family: 'vllm', instance_id: 'h200', type: :inference, model: colon_model
|
|
141
|
+
)
|
|
142
|
+
expect(build_selection(lane_id: id, model: colon_model).lane_id).to eq(id)
|
|
110
143
|
end
|
|
111
144
|
|
|
112
145
|
it 'rejects a publisher_token_id without the ptok:v1: prefix' do
|