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
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ # 09 §1 — canonical type conformance: the T-groups as shared examples.
4
+ #
5
+ # The including spec supplies:
6
+ # let(:type_class) — the Canonical type
7
+ # let(:type_source) — a Hash exercising the members (from_hash input)
8
+ # optionally:
9
+ # let(:auto_generated_members) — member names filled by the factory when
10
+ # absent from the source (id, timestamp), excluded from member-equality
11
+ # checks.
12
+ #
13
+ # Type-specific laws (T4 member survival, T5 enum law, T7 member normalization
14
+ # for the type's normalizable members) are asserted in each type spec.
15
+
16
+ RSpec.shared_examples 'a canonical type' do
17
+ let(:type) { type_class }
18
+ let(:built) { type.from_hash(type_source) }
19
+
20
+ describe 'T1 — strict input' do
21
+ it 'raises a typed ArgumentError on nil (no nil factory)' do
22
+ expect { type.from_hash(nil) }
23
+ .to raise_error(ArgumentError, /expected Hash, got NilClass/)
24
+ end
25
+
26
+ it 'raises a typed ArgumentError naming the offending class on wrong-class input' do
27
+ expect { type.from_hash('not a hash') }
28
+ .to raise_error(ArgumentError, /expected Hash, got String/)
29
+ expect { type.from_hash(42) }
30
+ .to raise_error(ArgumentError, /expected Hash, got Integer/)
31
+ end
32
+
33
+ it 'never returns nil from from_hash (valid instances or typed raises only)' do
34
+ expect(type.from_hash(type_source)).not_to be_nil
35
+ result =
36
+ begin
37
+ type.from_hash({})
38
+ rescue ArgumentError
39
+ :typed_raise
40
+ end
41
+ expect(result).not_to be_nil
42
+ end
43
+ end
44
+
45
+ describe 'T2 — unknown-key policy (04 L5)' do
46
+ it 'folds unknown keys into metadata verbatim — no drops, no raises' do
47
+ source = type_source.merge(fleet_unknown_key: 'kept', other_unknown: [1, 2])
48
+ instance = type.from_hash(source)
49
+
50
+ expect(instance.metadata).to include(fleet_unknown_key: 'kept', other_unknown: [1, 2])
51
+ end
52
+
53
+ it 'round-trips the folded unknowns' do
54
+ source = type_source.merge(fleet_unknown_key: 'kept')
55
+ instance = type.from_hash(source)
56
+ expect(type.from_hash(instance.to_h).metadata).to include(fleet_unknown_key: 'kept')
57
+ end
58
+ end
59
+
60
+ describe 'T3 — round-trip identity (04 L7)' do
61
+ it 'build → to_h → from_hash → Data-equal' do
62
+ expect(type.from_hash(built.to_h)).to eq(built)
63
+ end
64
+
65
+ it 'to_h is a plain Hash with symbol keys (no member lost, no key invented)' do
66
+ hash = built.to_h
67
+ expect(hash).to be_a(Hash)
68
+ expect(hash.keys).to all(be_a(Symbol))
69
+ checkable = (type.members.map(&:to_sym) - auto_generated_members)
70
+ checkable.each do |member|
71
+ value = built.public_send(member)
72
+ next if value.nil?
73
+
74
+ expect(hash.key?(member)).to be(true)
75
+ end
76
+ end
77
+ end
78
+
79
+ describe 'T6 — serialization uniformity (04 L8)' do
80
+ it 'as_json == to_h and to_json == to_h.to_json' do
81
+ expect(built.as_json).to eq(built.to_h)
82
+ expect(built.to_json).to eq(built.to_h.to_json)
83
+ end
84
+ end
85
+
86
+ # H1: .new is the single strict constructor — it runs the same member
87
+ # contract as the factories. Poison that passes an is_a? check is not
88
+ # constructible through ANY public path.
89
+ describe 'T8 — strict .new (H1: one validated constructor)' do
90
+ let(:member_kwargs) { type_source.transform_keys(&:to_sym) }
91
+
92
+ it 'rejects unknown members with a typed ArgumentError (no silent drops)' do
93
+ expect { type_class.new(**member_kwargs, not_a_member: true) }
94
+ .to raise_error(ArgumentError, /not_a_member/)
95
+ end
96
+
97
+ it 'rejects a non-Hash metadata member' do
98
+ expect { type_class.new(**member_kwargs, metadata: 'not-a-hash') }
99
+ .to raise_error(ArgumentError, /metadata expected Hash, got String/)
100
+ end
101
+
102
+ it 'constructs valid data (the factories and .new share one contract)' do
103
+ expect { type_class.new(**member_kwargs) }.not_to raise_error
104
+ end
105
+ end
106
+ end
@@ -5,10 +5,17 @@
5
5
  # Ship location: spec/legion/extensions/llm/conformance/
6
6
  # Module: Canonical::Conformance
7
7
  #
8
- # Consumer pattern (in provider gem spec_helper):
8
+ # Consumer pattern (in provider gem spec_helper) — load the kit's example
9
+ # modules BY EXPLICIT NAME. Do NOT glob the kit dir: it also ships lex-llm's
10
+ # own self-test specs (echo_translator_spec, ssot_*_conformance_spec) plus
11
+ # their support files, which LoadError outside this repo.
9
12
  # kit = File.join(Gem.loaded_specs['lex-llm'].full_gem_path,
10
13
  # 'spec/legion/extensions/llm/conformance')
11
- # Dir[File.join(kit, '**', '*.rb')].sort.each { |f| require f }
14
+ # %w[conformance.rb canonical_type_examples.rb client_translator_examples.rb
15
+ # provider_translator_examples.rb provider_tool_rendering_examples.rb
16
+ # ssot_contract_examples.rb ssot_provider_examples.rb].each do |f|
17
+ # require File.join(kit, f)
18
+ # end
12
19
  #
13
20
  # Then in specs:
14
21
  # it_behaves_like 'a canonical provider translator', described_class
@@ -47,5 +54,6 @@ module Canonical
47
54
  end
48
55
  end
49
56
 
57
+ require_relative 'canonical_type_examples'
50
58
  require_relative 'provider_translator_examples'
51
59
  require_relative 'client_translator_examples'
@@ -298,7 +298,7 @@ RSpec.shared_examples 'a canonical provider translator' do |translator_class|
298
298
  it 'preserves tool call identity across chunks' do
299
299
  tool_chunks = chunks.select { |c| c['type'] == 'tool_call_delta' }
300
300
  parsed_chunks = tool_chunks.map { |c| translator.parse_chunk(c) }
301
- ids = parsed_chunks.map { |c| c.tool_call&.id }
301
+ ids = parsed_chunks.map { |c| c.tool_call.is_a?(Hash) ? c.tool_call[:id] : c.tool_call&.id }
302
302
  expect(ids.uniq.length).to eq(1)
303
303
  end
304
304
  end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'legion/extensions/llm/inventory/registry'
5
+ require 'support/ssot_registry_helpers'
6
+ require_relative 'conformance'
7
+ require_relative 'ssot_contract_examples'
8
+
9
+ # Kit self-test: the B/F/R shared examples run against a real Provider-derived
10
+ # callable, the real Registry, and the real fleet responder/worker — proving
11
+ # the examples provider gems will load are executable and meaningful.
12
+ # rubocop:disable RSpec/DescribeClass, RSpec/MultipleMemoizedHelpers -- kit host
13
+ RSpec.describe 'SSOT v4 contract conformance kit' do
14
+ include SsotRegistryHelpers
15
+
16
+ let(:registry) { Legion::Extensions::Llm::Inventory::Registry }
17
+ let(:family) { :fake_llm }
18
+ let(:key) do
19
+ Legion::Extensions::Llm::Inventory::Identity::InstanceKey.new(provider_family: family, instance_id: 'primary')
20
+ end
21
+ let(:coordinator) { probe_coordinator(key) }
22
+ let(:activated) { true }
23
+
24
+ # A real 0.8.0 callable: Provider-derived, canonical in and out, no HTTP.
25
+ let(:callable) do
26
+ Class.new(Legion::Extensions::Llm::Provider) do
27
+ def self.slug = 'kit'
28
+ def self.configuration_requirements = []
29
+ def api_base = 'https://kit.invalid'
30
+
31
+ def chat(messages, model:, tools: nil, **_rest)
32
+ enforce_canonical_messages!(messages)
33
+ enforce_canonical_tools!(tools)
34
+ Legion::Extensions::Llm::Canonical::Response.build(text: 'ok', model: model, stop_reason: :end_turn)
35
+ end
36
+
37
+ # rubocop:disable Lint/UnusedMethodArgument -- model: mirrors the 0.8.0 callable contract
38
+ def stream_chat(messages, model: nil, tools: nil, **_contract, &block)
39
+ enforce_canonical_messages!(messages)
40
+ enforce_canonical_tools!(tools)
41
+ block&.call(Legion::Extensions::Llm::Canonical::Chunk.text_delta(delta: 'ok', request_id: nil))
42
+ block&.call(Legion::Extensions::Llm::Canonical::Chunk.done(request_id: nil, stop_reason: :end_turn,
43
+ usage: Legion::Extensions::Llm::Canonical::Usage.build(input_tokens: 1)))
44
+ nil
45
+ end
46
+ # rubocop:enable Lint/UnusedMethodArgument
47
+
48
+ def count_tokens(messages:, **_contract)
49
+ enforce_canonical_messages!(messages)
50
+ 1
51
+ end
52
+ end.new(Legion::Extensions::Llm.config)
53
+ end
54
+ let(:instance_key) { key }
55
+ let(:lane_id) { registry.snapshot.lanes_for(instance_key: key).first.lane_id }
56
+ # The fleet claim field keeps the name offering_id (D4 protocol continuity);
57
+ # its value is the lane's 5 tuple.
58
+ let(:offering_id) { lane_id }
59
+ let(:callable_handle) { registry.snapshot.instance(instance_key: key).callable_handle }
60
+ let(:operation_evidence_map) { operation_evidence }
61
+ let(:unknown_value) do
62
+ Legion::Extensions::Llm::Inventory::ValueEvidence.new(status: :unknown, source: :absent)
63
+ end
64
+ let(:exact_envelope_data) do
65
+ {
66
+ request_id: 'req-1', correlation_id: 'corr-1', idempotency_key: 'idem-1',
67
+ operation: 'chat', provider: family.to_s, provider_instance: 'primary', model: 'gemma4',
68
+ params: { messages: [{ role: 'user', content: 'hello' }] },
69
+ reply_to: 'reply.1', message_context: {}, caller: 'e2e', trace_context: {},
70
+ signed_token: 'unsigned', timeout_seconds: 30,
71
+ expires_at: (Time.now.utc + 120).iso8601,
72
+ protocol_version: Legion::Extensions::Llm::Fleet::Protocol::VERSION,
73
+ execution_contract: Legion::Extensions::Llm::Fleet::Protocol::EXACT_EXECUTION_CONTRACT,
74
+ offering_id: offering_id
75
+ }
76
+ end
77
+ let(:valid_request_options) do
78
+ {
79
+ routing_key: 'llm.fleet.inference.gemma4.ctx8192',
80
+ request_id: 'req-1', correlation_id: 'corr-1', idempotency_key: 'idem-1',
81
+ operation: :chat, provider: family.to_s, provider_instance: 'primary', model: 'gemma4',
82
+ params: { messages: [{ role: 'user', content: 'hello' }] },
83
+ reply_to: 'reply.1', message_context: {}, caller: 'e2e', trace_context: {},
84
+ signed_token: 'signed.jwt', timeout_seconds: 30, expires_at: '2026-08-20T12:00:00Z',
85
+ protocol_version: Legion::Extensions::Llm::Fleet::Protocol::VERSION,
86
+ execution_contract: Legion::Extensions::Llm::Fleet::Protocol::EXACT_EXECUTION_CONTRACT,
87
+ offering_id: offering_id
88
+ }
89
+ end
90
+
91
+ before do
92
+ registry.reset!
93
+ claim_and_activate(
94
+ key: key, callable: callable, coordinator: coordinator, model: 'gemma4',
95
+ weight_inputs: { tier: 100, provider: 50, instance: 2, model_or_offering: 1 },
96
+ base_weight: 10_000
97
+ )
98
+ allow(Legion::Extensions::Llm::Fleet::Settings).to receive(:value).and_call_original
99
+ allow(Legion::Extensions::Llm::Fleet::Settings).to receive(:value)
100
+ .with(:fleet, :auth, :require_signed_token, default: true).and_return(false)
101
+ allow(Legion::Extensions::Llm::Fleet::Settings).to receive(:value)
102
+ .with(:fleet, :responder, :require_idempotency, default: nil).and_return(false)
103
+ Legion::Extensions::Llm::Fleet::WorkerExecution.reset_idempotency_cache!
104
+ Legion::Extensions::Llm::Fleet::TokenValidator.reset_replay_cache!
105
+ end
106
+
107
+ it_behaves_like 'B1 — central canonical enforcement (08 F2)'
108
+ it_behaves_like 'B1b — central canonical tool enforcement (H3)'
109
+ it_behaves_like 'B2 — canonical outputs (05 O5, 08 R2)'
110
+ it_behaves_like 'B3 — operation preservation (PR #189 defect class)'
111
+ it_behaves_like 'B4 — no model re-derivation (PR #45 law)'
112
+ it_behaves_like 'B5 — no weight recomputation (PR #47 defect class)'
113
+ it_behaves_like 'B6 — zero-weight disable (U4)'
114
+ it_behaves_like 'B7 — health authority (RULES 9)'
115
+ it_behaves_like 'B8 — exact execution never downgrades (06 P2/W1)'
116
+ it_behaves_like 'B9 — no silent defaults (06 W5)'
117
+ it_behaves_like 'F1 — envelope round-trip (06 E2)'
118
+ it_behaves_like 'F2 — rehydration identity (06 W4, E01)'
119
+ it_behaves_like 'F3 — signing law (06 S3)'
120
+ it_behaves_like 'F4 — fencing (07 §3)'
121
+ it_behaves_like 'F5 — response envelope (06 E3/E4)'
122
+ it_behaves_like 'F6 — contract errors (06 §5)'
123
+ it_behaves_like 'F7 — retryability (06 F6)'
124
+ it_behaves_like 'R1 — state machine (07 §3)'
125
+ it_behaves_like 'R2 — identity (07 §1, 5-tuple law)'
126
+ it_behaves_like 'R3 — snapshot law (07 §5)'
127
+ it_behaves_like 'R4 — weight law (07 §4)'
128
+ it_behaves_like 'R5 — callable lifecycle (07 §6)'
129
+ end
130
+ # rubocop:enable RSpec/DescribeClass, RSpec/MultipleMemoizedHelpers