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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +139 -0
- data/RULES.md +97 -0
- data/lib/legion/extensions/llm/auto_registration.rb +4 -13
- data/lib/legion/extensions/llm/canonical/chunk.rb +107 -94
- data/lib/legion/extensions/llm/canonical/content_block.rb +65 -75
- data/lib/legion/extensions/llm/canonical/message.rb +53 -69
- data/lib/legion/extensions/llm/canonical/params.rb +58 -34
- data/lib/legion/extensions/llm/canonical/request.rb +68 -56
- data/lib/legion/extensions/llm/canonical/response.rb +62 -78
- data/lib/legion/extensions/llm/canonical/strict.rb +105 -0
- data/lib/legion/extensions/llm/canonical/thinking.rb +36 -84
- data/lib/legion/extensions/llm/canonical/thinking_config.rb +149 -0
- data/lib/legion/extensions/llm/canonical/tool_call.rb +43 -53
- 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 +7 -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 -156
- data/lib/legion/extensions/llm/fleet/token_validator.rb +15 -21
- data/lib/legion/extensions/llm/fleet/worker_execution.rb +107 -154
- 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 +36 -100
- data/lib/legion/extensions/llm/inventory/registry.rb +54 -48
- data/lib/legion/extensions/llm/inventory/snapshot.rb +7 -21
- data/lib/legion/extensions/llm/inventory/weight_reconciler.rb +13 -6
- data/lib/legion/extensions/llm/inventory/weight_schema.rb +47 -8
- data/lib/legion/extensions/llm/provider/open_ai_compatible.rb +178 -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 +48 -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 +14 -26
- 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 +8 -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 +55 -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/config_spec.rb +222 -0
- data/spec/legion/extensions/llm/canonical/thinking_spec.rb +23 -171
- 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/client_translator_examples.rb +1 -2
- data/spec/legion/extensions/llm/conformance/conformance.rb +10 -2
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_fleet_round_trip.json +1 -1
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_thinking_request.json +2 -2
- data/spec/legion/extensions/llm/conformance/provider_translator_examples.rb +1 -1
- data/spec/legion/extensions/llm/conformance/ssot_contract_conformance_spec.rb +128 -0
- data/spec/legion/extensions/llm/conformance/ssot_contract_examples.rb +505 -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 +173 -95
- data/spec/legion/extensions/llm/fleet/token_validator_spec.rb +7 -2
- data/spec/legion/extensions/llm/fleet/worker_execution_spec.rb +93 -74
- data/spec/legion/extensions/llm/fleet_messages_spec.rb +119 -125
- data/spec/legion/extensions/llm/gemspec_spec.rb +1 -2
- 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 +82 -47
- data/spec/legion/extensions/llm/inventory/registry_activation_spec.rb +49 -16
- 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 +7 -3
- data/spec/legion/extensions/llm/inventory/weight_schema_spec.rb +60 -10
- data/spec/legion/extensions/llm/provider/open_ai_compatible_spec.rb +128 -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 +97 -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 -43
- 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
- metadata +13 -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 -310
- 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 -340
- 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
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require_relative '../../conformance/conformance'
|
|
5
|
+
|
|
6
|
+
RSpec.describe Legion::Extensions::Llm::Canonical::Thinking::Config do
|
|
7
|
+
subject(:config_class) { described_class }
|
|
8
|
+
|
|
9
|
+
let(:type_class) { described_class }
|
|
10
|
+
let(:auto_generated_members) { [] }
|
|
11
|
+
let(:type_source) do
|
|
12
|
+
{ enabled: true, effort: 'high', budget: 4096, summary: :concise, metadata: { source: 'client' } }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it_behaves_like 'a canonical type'
|
|
16
|
+
|
|
17
|
+
it 'converts from a plain class to a Data with one name (04 §8)' do
|
|
18
|
+
expect(config_class).to be_a(Class)
|
|
19
|
+
expect(config_class.new(enabled: true, effort: 'low', budget: nil, summary: nil, metadata: {})).to be_a(config_class)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it 'normalizes symbol effort to String' do
|
|
23
|
+
config = config_class.build(effort: :low)
|
|
24
|
+
expect(config.effort).to eq('low')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'raises on a wrong-class effort' do
|
|
28
|
+
expect { config_class.build(effort: 3) }
|
|
29
|
+
.to raise_error(ArgumentError, /effort expected String, got Integer/)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
describe 'enabled member' do
|
|
33
|
+
it 'defaults to true in .build' do
|
|
34
|
+
config = config_class.build
|
|
35
|
+
expect(config.enabled).to be(true)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'defaults to true in .from_hash when key absent' do
|
|
39
|
+
config = config_class.from_hash({})
|
|
40
|
+
expect(config.enabled).to be(true)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'accepts explicit true' do
|
|
44
|
+
config = config_class.build(enabled: true)
|
|
45
|
+
expect(config.enabled).to be(true)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it 'accepts explicit false' do
|
|
49
|
+
config = config_class.build(enabled: false)
|
|
50
|
+
expect(config.enabled).to be(false)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'rejects non-boolean in .new' do
|
|
54
|
+
expect { config_class.new(enabled: 'yes', effort: nil, budget: nil, summary: nil, metadata: {}) }
|
|
55
|
+
.to raise_error(ArgumentError, /enabled must be true or false/)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'rejects nil in .new (must be an explicit boolean)' do
|
|
59
|
+
expect { config_class.new(enabled: nil, effort: nil, budget: nil, summary: nil, metadata: {}) }
|
|
60
|
+
.to raise_error(ArgumentError, /enabled must be true or false/)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it 'enabled? returns the enabled member' do
|
|
64
|
+
expect(config_class.build(enabled: true).enabled?).to be(true)
|
|
65
|
+
expect(config_class.build(enabled: false).enabled?).to be(false)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
describe 'H1/M4 — effort is a closed enum, validated in every constructor' do
|
|
70
|
+
it 'rejects an unrecognized effort in build, from_hash, and .new' do
|
|
71
|
+
expect { config_class.build(effort: 'banana') }
|
|
72
|
+
.to raise_error(ArgumentError, /Invalid effort: "banana"/)
|
|
73
|
+
expect { config_class.from_hash(effort: 'banana') }
|
|
74
|
+
.to raise_error(ArgumentError, /Invalid effort: "banana"/)
|
|
75
|
+
expect { config_class.new(enabled: true, effort: 'banana', budget: nil, summary: nil, metadata: {}) }
|
|
76
|
+
.to raise_error(ArgumentError, /Invalid effort: "banana"/)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it 'accepts the enum case-insensitively (symbol or string)' do
|
|
80
|
+
expect(config_class.build(effort: :HIGH).effort).to eq('high')
|
|
81
|
+
expect(config_class.new(enabled: true, effort: 'Medium', budget: nil, summary: nil, metadata: {}).effort).to eq('medium')
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it 'accepts all six effort levels' do
|
|
85
|
+
%w[none low medium high xhigh max].each do |level|
|
|
86
|
+
expect(config_class.build(effort: level).effort).to eq(level)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it 'resolved_budget derives from the validated effort (no silent medium fallback)' do
|
|
91
|
+
expect(config_class.build(effort: 'low').resolved_budget).to eq(1024)
|
|
92
|
+
expect(config_class.build(effort: 'medium').resolved_budget).to eq(8192)
|
|
93
|
+
expect(config_class.build(effort: 'high').resolved_budget).to eq(16_384)
|
|
94
|
+
expect(config_class.build(effort: 'xhigh').resolved_budget).to eq(24_576)
|
|
95
|
+
expect(config_class.build(effort: 'max').resolved_budget).to eq(32_768)
|
|
96
|
+
expect(config_class.build(effort: 'none').resolved_budget).to be_nil
|
|
97
|
+
expect(config_class.build(budget: 123).resolved_budget).to eq(123)
|
|
98
|
+
expect(config_class.build(enabled: false).resolved_budget).to be_nil
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
describe 'budget validation' do
|
|
103
|
+
it 'accepts a positive integer' do
|
|
104
|
+
expect(config_class.build(budget: 1).budget).to eq(1)
|
|
105
|
+
expect(config_class.build(budget: 50_000).budget).to eq(50_000)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it 'accepts nil' do
|
|
109
|
+
expect(config_class.build(budget: nil).budget).to be_nil
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it 'rejects zero' do
|
|
113
|
+
expect { config_class.build(budget: 0) }
|
|
114
|
+
.to raise_error(ArgumentError, /budget must be positive/)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it 'rejects negative integers' do
|
|
118
|
+
expect { config_class.build(budget: -1) }
|
|
119
|
+
.to raise_error(ArgumentError, /budget must be positive/)
|
|
120
|
+
expect { config_class.build(budget: -100) }
|
|
121
|
+
.to raise_error(ArgumentError, /budget must be positive/)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it 'rejects non-Integer types' do
|
|
125
|
+
expect { config_class.build(budget: 1.5) }
|
|
126
|
+
.to raise_error(ArgumentError, /budget expected Integer/)
|
|
127
|
+
expect { config_class.build(budget: '1024') }
|
|
128
|
+
.to raise_error(ArgumentError, /budget expected Integer/)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
describe 'summary enum' do
|
|
133
|
+
it 'accepts nil (default)' do
|
|
134
|
+
expect(config_class.build(summary: nil).summary).to be_nil
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it 'accepts :auto, :none, :concise, :detailed' do
|
|
138
|
+
%i[auto none concise detailed].each do |level|
|
|
139
|
+
expect(config_class.build(summary: level).summary).to eq(level)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it 'coerces string to symbol' do
|
|
144
|
+
expect(config_class.from_hash(summary: 'concise').summary).to eq(:concise)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
it 'rejects invalid summary values' do
|
|
148
|
+
expect { config_class.build(summary: :verbose) }
|
|
149
|
+
.to raise_error(ArgumentError, /Invalid summary/)
|
|
150
|
+
expect { config_class.build(summary: :full) }
|
|
151
|
+
.to raise_error(ArgumentError, /Invalid summary/)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
it 'rejects non-symbol/string types' do
|
|
155
|
+
expect { config_class.build(summary: 42) }
|
|
156
|
+
.to raise_error(ArgumentError, /summary expected Symbol/)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
it 'keeps the effort<->budget SSOT conversions' do
|
|
161
|
+
expect(config_class.build(effort: 'low').resolved_budget).to eq(1024)
|
|
162
|
+
expect(config_class.build(effort: 'medium').resolved_budget).to eq(8192)
|
|
163
|
+
expect(config_class.build(effort: 'high').resolved_budget).to eq(16_384)
|
|
164
|
+
expect(config_class.build(effort: 'xhigh').resolved_budget).to eq(24_576)
|
|
165
|
+
expect(config_class.build(effort: 'max').resolved_budget).to eq(32_768)
|
|
166
|
+
expect(config_class.build(budget: 512).resolved_effort).to eq('low')
|
|
167
|
+
expect(config_class.build(budget: 1024).resolved_effort).to eq('low')
|
|
168
|
+
expect(config_class.build(budget: 8192).resolved_effort).to eq('medium')
|
|
169
|
+
expect(config_class.build(budget: 16_384).resolved_effort).to eq('high')
|
|
170
|
+
expect(config_class.build(budget: 24_576).resolved_effort).to eq('xhigh')
|
|
171
|
+
expect(config_class.build(budget: 32_768).resolved_effort).to eq('max')
|
|
172
|
+
expect(config_class.build(budget: 50_000).resolved_effort).to eq('max')
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
describe 'resolved_effort band boundaries' do
|
|
176
|
+
it 'maps budget at boundary to the correct effort level' do
|
|
177
|
+
# At the low boundary
|
|
178
|
+
expect(config_class.build(budget: 1024).resolved_effort).to eq('low')
|
|
179
|
+
# Just above low
|
|
180
|
+
expect(config_class.build(budget: 1025).resolved_effort).to eq('medium')
|
|
181
|
+
# At medium boundary
|
|
182
|
+
expect(config_class.build(budget: 8192).resolved_effort).to eq('medium')
|
|
183
|
+
# Just above medium
|
|
184
|
+
expect(config_class.build(budget: 8193).resolved_effort).to eq('high')
|
|
185
|
+
# At high boundary
|
|
186
|
+
expect(config_class.build(budget: 16_384).resolved_effort).to eq('high')
|
|
187
|
+
# Just above high
|
|
188
|
+
expect(config_class.build(budget: 16_385).resolved_effort).to eq('xhigh')
|
|
189
|
+
# At xhigh boundary
|
|
190
|
+
expect(config_class.build(budget: 24_576).resolved_effort).to eq('xhigh')
|
|
191
|
+
# Just above xhigh
|
|
192
|
+
expect(config_class.build(budget: 24_577).resolved_effort).to eq('max')
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
it 'returns nil when budget is nil (no fabrication)' do
|
|
196
|
+
expect(config_class.build.resolved_effort).to be_nil
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
it 'returns the supplied effort when both are set (no overwrite)' do
|
|
200
|
+
config = config_class.build(effort: 'low', budget: 30_000)
|
|
201
|
+
expect(config.resolved_effort).to eq('low')
|
|
202
|
+
expect(config.resolved_budget).to eq(30_000)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
it 'to_h is faithful to what was set (no fabricated axis)' do
|
|
207
|
+
expect(config_class.build(effort: 'low').to_h).to eq(enabled: true, effort: 'low', metadata: {})
|
|
208
|
+
# enabled:false is explicit OFF signal — it IS set, so it appears in to_h
|
|
209
|
+
expect(config_class.build(enabled: false).to_h).to eq(enabled: false, metadata: {})
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
it 'to_h omits nil members (compact)' do
|
|
213
|
+
config = config_class.build(enabled: true, effort: 'high', budget: 4096, summary: :detailed)
|
|
214
|
+
expect(config.to_h).to eq(enabled: true, effort: 'high', budget: 4096, summary: :detailed, metadata: {})
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
it 'from_hash round-trips through to_h' do
|
|
218
|
+
original = config_class.build(enabled: true, effort: 'xhigh', budget: 24_576, summary: :auto)
|
|
219
|
+
rebuilt = config_class.from_hash(original.to_h)
|
|
220
|
+
expect(rebuilt).to eq(original)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
@@ -1,189 +1,41 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'spec_helper'
|
|
4
|
+
require_relative '../conformance/conformance'
|
|
4
5
|
|
|
5
6
|
RSpec.describe Legion::Extensions::Llm::Canonical::Thinking do
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
expect(thinking.content).to eq('reasoning here')
|
|
12
|
-
expect(thinking.signature).to eq('sig-abc')
|
|
13
|
-
end
|
|
7
|
+
let(:type_class) { described_class }
|
|
8
|
+
let(:auto_generated_members) { [] }
|
|
9
|
+
let(:type_source) do
|
|
10
|
+
{ content: 'reasoning here', signature: 'sig-abc', metadata: { origin: 'provider' } }
|
|
11
|
+
end
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
thinking = described_class.from_hash('content' => 'reasoning', 'signature' => 'sig-123')
|
|
13
|
+
it_behaves_like 'a canonical type'
|
|
17
14
|
|
|
15
|
+
describe 'T5/T4 — construction law' do
|
|
16
|
+
it 'builds from keyword args' do
|
|
17
|
+
thinking = described_class.build(content: 'reasoning', signature: 'sig')
|
|
18
18
|
expect(thinking.content).to eq('reasoning')
|
|
19
|
-
expect(thinking.signature).to eq('sig
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
it 'returns nil for nil source' do
|
|
23
|
-
expect(described_class.from_hash(nil)).to be_nil
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
it 'returns nil for empty content and signature' do
|
|
27
|
-
result = described_class.from_hash(content: '', signature: '')
|
|
28
|
-
|
|
29
|
-
expect(result).to be_nil
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
it 'returns nil when both fields are nil' do
|
|
33
|
-
result = described_class.from_hash(content: nil, signature: nil)
|
|
34
|
-
|
|
35
|
-
expect(result).to be_nil
|
|
19
|
+
expect(thinking.signature).to eq('sig')
|
|
36
20
|
end
|
|
37
21
|
|
|
38
|
-
it '
|
|
39
|
-
thinking = described_class.
|
|
40
|
-
|
|
41
|
-
expect(thinking.content).to eq('just reasoning')
|
|
42
|
-
expect(thinking.signature).to be_nil
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
it 'returns instance with only signature' do
|
|
46
|
-
thinking = described_class.from_hash(signature: 'sig-only')
|
|
47
|
-
|
|
22
|
+
it 'normalizes empty strings to nil (absence, not data — 04 §8)' do
|
|
23
|
+
thinking = described_class.build(content: '', signature: '')
|
|
48
24
|
expect(thinking.content).to be_nil
|
|
49
|
-
expect(thinking.signature).to
|
|
50
|
-
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
describe '#to_h' do
|
|
54
|
-
it 'serializes to compact hash' do
|
|
55
|
-
thinking = described_class.new(content: 'reasoning', signature: 'sig-1')
|
|
56
|
-
hash = thinking.to_h
|
|
57
|
-
|
|
58
|
-
expect(hash).to eq(content: 'reasoning', signature: 'sig-1')
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
it 'omits nil values' do
|
|
62
|
-
thinking = described_class.new(content: 'reasoning', signature: nil)
|
|
63
|
-
hash = thinking.to_h
|
|
64
|
-
|
|
65
|
-
expect(hash).to eq(content: 'reasoning')
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
describe '#empty?' do
|
|
70
|
-
it 'returns true when both fields are nil' do
|
|
71
|
-
thinking = described_class.new(content: nil, signature: nil)
|
|
72
|
-
expect(thinking.empty?).to be true
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
it 'returns false when content is present' do
|
|
76
|
-
thinking = described_class.new(content: 'reasoning', signature: nil)
|
|
77
|
-
expect(thinking.empty?).to be false
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
describe 'round-trip' do
|
|
82
|
-
it 'preserves content and signature through from_hash/to_h' do
|
|
83
|
-
original = { content: 'deep reasoning', signature: 'sig-xyz' }
|
|
84
|
-
thinking = described_class.from_hash(original)
|
|
85
|
-
serialized = thinking.to_h
|
|
86
|
-
|
|
87
|
-
expect(serialized).to eq(original)
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
describe '::Config' do
|
|
92
|
-
let(:config_class) { Legion::Extensions::Llm::Canonical::Thinking::Config }
|
|
93
|
-
|
|
94
|
-
describe '.build' do
|
|
95
|
-
it 'creates a config with effort and budget' do
|
|
96
|
-
config = config_class.build(effort: 'high', budget: 10_000)
|
|
97
|
-
|
|
98
|
-
expect(config.effort).to eq('high')
|
|
99
|
-
expect(config.budget).to eq(10_000)
|
|
100
|
-
expect(config.enabled?).to be true
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
it 'converts symbol effort to string' do
|
|
104
|
-
config = config_class.build(effort: :high)
|
|
105
|
-
|
|
106
|
-
expect(config.effort).to eq('high')
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
it 'creates disabled config when no values' do
|
|
110
|
-
config = config_class.build
|
|
111
|
-
|
|
112
|
-
expect(config.enabled?).to be false
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
describe '.from_hash' do
|
|
117
|
-
it 'parses config from hash' do
|
|
118
|
-
config = config_class.from_hash(effort: 'medium', budget: 5000)
|
|
119
|
-
|
|
120
|
-
expect(config.effort).to eq('medium')
|
|
121
|
-
expect(config.budget).to eq(5000)
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
it 'handles string keys' do
|
|
125
|
-
config = config_class.from_hash('effort' => 'low')
|
|
126
|
-
|
|
127
|
-
expect(config.effort).to eq('low')
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
it 'returns nil for nil source' do
|
|
131
|
-
expect(config_class.from_hash(nil)).to be_nil
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
it 'returns nil for empty hash' do
|
|
135
|
-
expect(config_class.from_hash({})).to be_nil
|
|
136
|
-
end
|
|
25
|
+
expect(thinking.signature).to be_nil
|
|
26
|
+
expect(thinking.empty?).to be(true)
|
|
137
27
|
end
|
|
138
28
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
expect(config.to_h).to eq(effort: 'high', budget: 10_000)
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
it 'omits nil values' do
|
|
146
|
-
config = config_class.build(effort: 'low')
|
|
147
|
-
expect(config.to_h).to eq(effort: 'low')
|
|
148
|
-
end
|
|
29
|
+
it 'raises on a wrong-class member' do
|
|
30
|
+
expect { described_class.build(content: 42) }
|
|
31
|
+
.to raise_error(ArgumentError, /content expected String, got Integer/)
|
|
149
32
|
end
|
|
150
33
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
# config), not scattered/one-directional in each translator. Derived accessors
|
|
157
|
-
# do NOT mutate stored state or #to_h — they only fill the gap on read.
|
|
158
|
-
describe 'derived cross-axis accessors' do
|
|
159
|
-
it 'derives budget from effort when only effort was set' do
|
|
160
|
-
expect(config_class.build(effort: 'low').resolved_budget).to eq(1024)
|
|
161
|
-
expect(config_class.build(effort: 'medium').resolved_budget).to eq(8192)
|
|
162
|
-
expect(config_class.build(effort: 'high').resolved_budget).to eq(16_384)
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
it 'derives effort from budget by band (< medium=low, < high=medium, else high)' do
|
|
166
|
-
expect(config_class.build(budget: 500).resolved_effort).to eq('low') # < 8192
|
|
167
|
-
expect(config_class.build(budget: 10_000).resolved_effort).to eq('medium') # 8192..16383
|
|
168
|
-
expect(config_class.build(budget: 20_000).resolved_effort).to eq('high') # >= 16384
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
it 'prefers the explicitly-set value over derivation' do
|
|
172
|
-
c = config_class.build(effort: 'high', budget: 2048)
|
|
173
|
-
expect(c.resolved_budget).to eq(2048)
|
|
174
|
-
expect(c.resolved_effort).to eq('high')
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
it 'returns nil derivations when neither axis is set' do
|
|
178
|
-
c = config_class.build
|
|
179
|
-
expect(c.resolved_budget).to be_nil
|
|
180
|
-
expect(c.resolved_effort).to be_nil
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
it 'does not fabricate the missing axis in to_h (stored state stays faithful)' do
|
|
184
|
-
expect(config_class.build(effort: 'high').to_h).to eq(effort: 'high')
|
|
185
|
-
expect(config_class.build(budget: 2048).to_h).to eq(budget: 2048)
|
|
186
|
-
end
|
|
34
|
+
it 'survives build → to_h → JSON → from_hash (T4 signature)' do
|
|
35
|
+
thinking = described_class.build(content: 'x', signature: 'sig-1')
|
|
36
|
+
round_tripped = described_class.from_hash(Legion::JSON.load(Legion::JSON.dump(thinking.to_h)))
|
|
37
|
+
expect(round_tripped.content).to eq('x')
|
|
38
|
+
expect(round_tripped.signature).to eq('sig-1')
|
|
187
39
|
end
|
|
188
40
|
end
|
|
189
41
|
end
|