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
|
@@ -11,20 +11,20 @@ module Legion
|
|
|
11
11
|
# A structurally immutable, generation-tagged read model of the registry.
|
|
12
12
|
# Lookups return the frozen record or nil and never synthesize a default.
|
|
13
13
|
# `instances_by_key` contains only activated instances;
|
|
14
|
-
# `publication_status_by_key` also contains initializing claims.
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
14
|
+
# `publication_status_by_key` also contains initializing claims. Lanes
|
|
15
|
+
# are keyed by the 5-tuple lane id and iterate in lexicographic id
|
|
16
|
+
# order (readable iteration). A caller cannot observe a later
|
|
17
|
+
# inventory-record or generation mutation through an older snapshot;
|
|
18
|
+
# the captured CallableHandle lifecycle is the single intentional
|
|
19
|
+
# exception. See phase-1-lex-llm-additive.md section 11.3.
|
|
18
20
|
class Snapshot
|
|
19
21
|
attr_reader :generation
|
|
20
22
|
|
|
21
|
-
def initialize(generation:, instances_by_key:,
|
|
23
|
+
def initialize(generation:, instances_by_key:, lanes_by_id:, publication_status_by_key:)
|
|
22
24
|
@generation = generation
|
|
23
25
|
@instances_by_key = order_instances(instances_by_key).freeze
|
|
24
|
-
@offerings_by_id = offerings_by_id.dup.freeze
|
|
25
26
|
@lanes_by_id = lanes_by_id.dup.freeze
|
|
26
27
|
@publication_status_by_key = order_instances(publication_status_by_key).freeze
|
|
27
|
-
@ordered_offerings = @offerings_by_id.keys.sort.map { |id| @offerings_by_id[id] }.freeze
|
|
28
28
|
@ordered_lanes = @lanes_by_id.keys.sort.map { |id| @lanes_by_id[id] }.freeze
|
|
29
29
|
freeze
|
|
30
30
|
end
|
|
@@ -33,10 +33,6 @@ module Legion
|
|
|
33
33
|
@instances_by_key[instance_key]
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
-
def offering(offering_id:)
|
|
37
|
-
@offerings_by_id[offering_id]
|
|
38
|
-
end
|
|
39
|
-
|
|
40
36
|
def lane(lane_id:)
|
|
41
37
|
@lanes_by_id[lane_id]
|
|
42
38
|
end
|
|
@@ -45,10 +41,6 @@ module Legion
|
|
|
45
41
|
@ordered_lanes.select { |lane| lane.instance_key == instance_key }.freeze
|
|
46
42
|
end
|
|
47
43
|
|
|
48
|
-
def offerings_for(instance_key:)
|
|
49
|
-
@ordered_offerings.select { |offering| offering.instance_key == instance_key }.freeze
|
|
50
|
-
end
|
|
51
|
-
|
|
52
44
|
def publication_status(instance_key:)
|
|
53
45
|
@publication_status_by_key[instance_key]
|
|
54
46
|
end
|
|
@@ -59,12 +51,6 @@ module Legion
|
|
|
59
51
|
@ordered_lanes.each(&block)
|
|
60
52
|
end
|
|
61
53
|
|
|
62
|
-
def each_offering(&block)
|
|
63
|
-
return to_enum(:each_offering) unless block
|
|
64
|
-
|
|
65
|
-
@ordered_offerings.each(&block)
|
|
66
|
-
end
|
|
67
|
-
|
|
68
54
|
def each_instance(&block)
|
|
69
55
|
return to_enum(:each_instance) unless block
|
|
70
56
|
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Direct dependency; the explicit receiver keeps Ruby 4's redundant-require cop from deleting it.
|
|
4
|
+
Kernel.require 'set'
|
|
5
|
+
require 'legion/extensions/llm/inventory/weight_schema'
|
|
6
|
+
require 'legion/extensions/llm/settings_cascade'
|
|
7
|
+
require 'legion/extensions/llm/inventory/identity'
|
|
8
|
+
require 'legion/extensions/llm/taxonomies'
|
|
9
|
+
|
|
10
|
+
module Legion
|
|
11
|
+
module Extensions
|
|
12
|
+
module Llm
|
|
13
|
+
module Inventory
|
|
14
|
+
# Tracks configured weight keys that currently have no published lane.
|
|
15
|
+
class DormantWeightTracker
|
|
16
|
+
def initialize
|
|
17
|
+
@dormant = Set.new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def observe(configured_keys:, published_keys:)
|
|
21
|
+
current = Set.new(configured_keys).difference(published_keys)
|
|
22
|
+
newly_dormant = current.difference(@dormant).sort_by(&:inspect)
|
|
23
|
+
@dormant.replace(current)
|
|
24
|
+
newly_dormant
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def clear!
|
|
28
|
+
@dormant.clear
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Shared atomic write-time weight publication for existing writer cadences.
|
|
33
|
+
module WeightReconciler
|
|
34
|
+
module_function
|
|
35
|
+
|
|
36
|
+
def rebuild_offerings(settings:, instance_key:, offerings:)
|
|
37
|
+
offerings.map do |draft|
|
|
38
|
+
inputs = WeightSchema.weight_inputs(
|
|
39
|
+
settings: settings,
|
|
40
|
+
instance_key: instance_key,
|
|
41
|
+
model: draft.model,
|
|
42
|
+
tier: draft.tier,
|
|
43
|
+
operation_evidence: draft.operation_evidence
|
|
44
|
+
)
|
|
45
|
+
draft.with(weight_inputs: inputs, base_weight: WeightSchema.base_weight(inputs))
|
|
46
|
+
end.freeze
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Periodic discovery calls this AFTER its existing network/discovery work. Read
|
|
50
|
+
# current Settings and perform weight rebuilding, comparison, sequence
|
|
51
|
+
# allocation, publish, and cache update under the same writer mutex. No Settings
|
|
52
|
+
# callback or separate reweight path exists.
|
|
53
|
+
def commit_if_changed!(settings:, instance_id:, state:, discovered_offerings:, **publication)
|
|
54
|
+
unknown = publication.keys - %i[mutex equivalent replace stable_signature]
|
|
55
|
+
raise ArgumentError, "unknown publication keyword(s): #{unknown.join(', ')}" unless unknown.empty?
|
|
56
|
+
|
|
57
|
+
mutex = publication.fetch(:mutex)
|
|
58
|
+
equivalent = publication.fetch(:equivalent)
|
|
59
|
+
replace = publication.fetch(:replace)
|
|
60
|
+
stable_signature = publication[:stable_signature]
|
|
61
|
+
mutex.synchronize do
|
|
62
|
+
offerings = rebuild_offerings(
|
|
63
|
+
settings: settings, instance_key: state.fetch(:instance_key),
|
|
64
|
+
offerings: discovered_offerings
|
|
65
|
+
)
|
|
66
|
+
return false if equivalent.call(state.fetch(:offerings), offerings)
|
|
67
|
+
|
|
68
|
+
if state.fetch(:published)
|
|
69
|
+
commit_locked!(
|
|
70
|
+
instance_id: instance_id, state: state, offerings: offerings,
|
|
71
|
+
replace: replace, stable_signature: stable_signature
|
|
72
|
+
)
|
|
73
|
+
else
|
|
74
|
+
cache_locked!(
|
|
75
|
+
state: state, offerings: offerings, stable_signature: stable_signature
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
true
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Track a claimed but not-yet-activated instance before its readiness I/O. An
|
|
83
|
+
# ordinary writer pass may refresh its cached weights, but must never send a
|
|
84
|
+
# replacement for a publication that is still :initializing.
|
|
85
|
+
def track_initializing!(states:, state_key:, state:, mutex:)
|
|
86
|
+
mutex.synchronize do
|
|
87
|
+
state[:published] = false
|
|
88
|
+
states[state_key] = state
|
|
89
|
+
end
|
|
90
|
+
state
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Initial and recovery activation both use this helper after readiness succeeds.
|
|
94
|
+
# Rebuild from current settings and publish under the writer's publication
|
|
95
|
+
# mutex, then mark the state published only after the publisher call succeeds.
|
|
96
|
+
def activate_tracked!(settings:, instance_id:, state_key:, state:, **activation)
|
|
97
|
+
unknown = activation.keys - %i[states mutex probe_token activate activation_sequence stable_signature]
|
|
98
|
+
raise ArgumentError, "unknown activation keyword(s): #{unknown.join(', ')}" unless unknown.empty?
|
|
99
|
+
|
|
100
|
+
states = activation.fetch(:states)
|
|
101
|
+
mutex = activation.fetch(:mutex)
|
|
102
|
+
probe_token = activation.fetch(:probe_token)
|
|
103
|
+
activate = activation.fetch(:activate)
|
|
104
|
+
activation_sequence = activation.fetch(:activation_sequence)
|
|
105
|
+
stable_signature = activation[:stable_signature]
|
|
106
|
+
mutex.synchronize do
|
|
107
|
+
return false unless states[state_key].equal?(state)
|
|
108
|
+
|
|
109
|
+
offerings = rebuild_offerings(
|
|
110
|
+
settings: settings, instance_key: state.fetch(:instance_key),
|
|
111
|
+
offerings: state.fetch(:offerings)
|
|
112
|
+
)
|
|
113
|
+
sequence = activation_sequence.call(state)
|
|
114
|
+
activate.call(
|
|
115
|
+
instance_id: instance_id, state: state, offerings: offerings,
|
|
116
|
+
sequence: sequence, probe_token: probe_token
|
|
117
|
+
)
|
|
118
|
+
state[:sequence] = sequence
|
|
119
|
+
cache_locked!(
|
|
120
|
+
state: state, offerings: offerings, stable_signature: stable_signature
|
|
121
|
+
)
|
|
122
|
+
state[:published] = true
|
|
123
|
+
true
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Ordinary ticks call this after reconcile. Dormant observation has the same
|
|
128
|
+
# cadence as the existing writer; there is no reload callback path.
|
|
129
|
+
def observe_dormant!(settings:, provider_family:, states:, mutex:, **observation)
|
|
130
|
+
unknown = observation.keys - %i[tracker dormant_logger]
|
|
131
|
+
raise ArgumentError, "unknown observation keyword(s): #{unknown.join(', ')}" unless unknown.empty?
|
|
132
|
+
|
|
133
|
+
tracker = observation.fetch(:tracker)
|
|
134
|
+
dormant_logger = observation.fetch(:dormant_logger)
|
|
135
|
+
mutex.synchronize do
|
|
136
|
+
observe_dormant_locked!(
|
|
137
|
+
settings: settings, provider_family: provider_family, states: states,
|
|
138
|
+
tracker: tracker, dormant_logger: dormant_logger
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def commit_locked!(instance_id:, state:, offerings:, replace:, stable_signature:)
|
|
144
|
+
sequence = state.fetch(:sequence) + 1
|
|
145
|
+
replace.call(instance_id: instance_id, state: state, offerings: offerings, sequence: sequence)
|
|
146
|
+
state[:sequence] = sequence
|
|
147
|
+
cache_locked!(
|
|
148
|
+
state: state, offerings: offerings, stable_signature: stable_signature
|
|
149
|
+
)
|
|
150
|
+
end
|
|
151
|
+
private_class_method :commit_locked!
|
|
152
|
+
|
|
153
|
+
def cache_locked!(state:, offerings:, stable_signature:)
|
|
154
|
+
state[:offerings] = offerings
|
|
155
|
+
state[:signature] = stable_signature.call(offerings) if stable_signature
|
|
156
|
+
end
|
|
157
|
+
private_class_method :cache_locked!
|
|
158
|
+
|
|
159
|
+
def observe_dormant_locked!(settings:, provider_family:, states:, tracker:,
|
|
160
|
+
dormant_logger:)
|
|
161
|
+
configured = configured_weight_keys(settings: settings, provider_family: provider_family)
|
|
162
|
+
published = published_weight_keys(provider_family: provider_family, states: states)
|
|
163
|
+
tracker.observe(configured_keys: configured, published_keys: published).each do |key|
|
|
164
|
+
dormant_logger.call(key)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
private_class_method :observe_dormant_locked!
|
|
168
|
+
|
|
169
|
+
def configured_weight_keys(settings:, provider_family:)
|
|
170
|
+
llm = config_hash(settings.dig(:extensions, :llm))
|
|
171
|
+
provider = config_hash(SettingsCascade.lookup(llm, provider_family))
|
|
172
|
+
keys = Set.new
|
|
173
|
+
keys << canonical_key(provider_family, :provider) if weight_present?(provider)
|
|
174
|
+
|
|
175
|
+
config_hash(SettingsCascade.lookup(provider, :models)).each do |model, config|
|
|
176
|
+
keys << canonical_key(provider_family, :model, model.to_s) if weight_present?(config_hash(config))
|
|
177
|
+
end
|
|
178
|
+
config_hash(SettingsCascade.lookup(provider, :instances)).each do |instance, config|
|
|
179
|
+
instance_config = config_hash(config)
|
|
180
|
+
keys << canonical_key(provider_family, :instance, instance.to_s) if weight_present?(instance_config)
|
|
181
|
+
config_hash(SettingsCascade.lookup(instance_config, :models)).each do |model, model_config|
|
|
182
|
+
next unless weight_present?(config_hash(model_config))
|
|
183
|
+
|
|
184
|
+
keys << canonical_key(
|
|
185
|
+
provider_family, :instance, instance.to_s, :model, model.to_s
|
|
186
|
+
)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
config_hash(SettingsCascade.lookup(provider, :offerings)).each do |offering_id, config|
|
|
190
|
+
keys << canonical_key(provider_family, :offering, offering_id.to_s) \
|
|
191
|
+
if weight_present?(config_hash(config))
|
|
192
|
+
end
|
|
193
|
+
keys
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def published_weight_keys(provider_family:, states:)
|
|
197
|
+
keys = Set.new
|
|
198
|
+
states.each_value do |state|
|
|
199
|
+
next unless state.fetch(:published)
|
|
200
|
+
|
|
201
|
+
offerings = Array(state[:offerings])
|
|
202
|
+
next if offerings.empty?
|
|
203
|
+
|
|
204
|
+
instance_key = state.fetch(:instance_key)
|
|
205
|
+
keys << canonical_key(provider_family, :provider)
|
|
206
|
+
keys << canonical_key(provider_family, :instance, instance_key.instance_id.to_s)
|
|
207
|
+
offerings.each do |draft|
|
|
208
|
+
keys << canonical_key(provider_family, :model, draft.model.to_s)
|
|
209
|
+
keys << canonical_key(
|
|
210
|
+
provider_family, :instance, instance_key.instance_id.to_s,
|
|
211
|
+
:model, draft.model.to_s
|
|
212
|
+
)
|
|
213
|
+
draft.operation_evidence.each_value do |evidence|
|
|
214
|
+
next unless evidence.supported?
|
|
215
|
+
|
|
216
|
+
lane_id = Identity.compose_lane_id(
|
|
217
|
+
tier: draft.tier, provider_family: instance_key.provider_family,
|
|
218
|
+
instance_id: instance_key.instance_id,
|
|
219
|
+
type: Taxonomies.lane_type_for(operation: evidence.operation), model: draft.model
|
|
220
|
+
)
|
|
221
|
+
keys << canonical_key(provider_family, :offering, lane_id)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
keys
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def canonical_key(provider_family, *parts)
|
|
229
|
+
[provider_family.to_sym, *parts].freeze
|
|
230
|
+
end
|
|
231
|
+
private_class_method :canonical_key
|
|
232
|
+
|
|
233
|
+
def config_hash(value)
|
|
234
|
+
return {} if value.nil?
|
|
235
|
+
return value if value.is_a?(::Hash)
|
|
236
|
+
|
|
237
|
+
raise ArgumentError, "weight configuration scope must be a Hash, got #{value.inspect}"
|
|
238
|
+
end
|
|
239
|
+
private_class_method :config_hash
|
|
240
|
+
|
|
241
|
+
def weight_present?(scope)
|
|
242
|
+
!SettingsCascade.lookup(scope, :weight).nil?
|
|
243
|
+
end
|
|
244
|
+
private_class_method :weight_present?
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/llm/settings_cascade'
|
|
4
|
+
require 'legion/extensions/llm/inventory/identity'
|
|
5
|
+
require 'legion/extensions/llm/taxonomies'
|
|
6
|
+
|
|
7
|
+
module Legion
|
|
8
|
+
module Extensions
|
|
9
|
+
module Llm
|
|
10
|
+
module Inventory
|
|
11
|
+
# Write-time lane-weight computation (RANKING v2 law): the stateless router
|
|
12
|
+
# reads the stored scalar; it is computed ONLY here, at write events, from a
|
|
13
|
+
# live settings read. Each multiplicative axis reads ITS OWN scope — never a
|
|
14
|
+
# fall-through cascade (a fall-through would double-count the other axes).
|
|
15
|
+
# The offering-scope settings key is the lane's 5 tuple — operator-readable,
|
|
16
|
+
# composed via Identity.compose_lane_id.
|
|
17
|
+
module WeightSchema
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
IDENTITY = 100
|
|
21
|
+
|
|
22
|
+
# Exact 4-key hash. The offering-scope settings key is the 5 tuple of
|
|
23
|
+
# the lanes the draft's operation evidence supports (one per distinct
|
|
24
|
+
# lane type) — finally operator-usable, no digest.
|
|
25
|
+
def weight_inputs(settings:, instance_key:, model:, tier:, operation_evidence:)
|
|
26
|
+
llm_conf = scope_hash(settings.dig(:extensions, :llm), path: 'extensions.llm')
|
|
27
|
+
provider_conf = scope_hash(
|
|
28
|
+
SettingsCascade.lookup(llm_conf, instance_key.provider_family),
|
|
29
|
+
path: "extensions.llm.#{instance_key.provider_family}"
|
|
30
|
+
)
|
|
31
|
+
instances = scope_hash(
|
|
32
|
+
SettingsCascade.lookup(provider_conf, :instances), path: 'provider.instances'
|
|
33
|
+
)
|
|
34
|
+
instance_cfg = scope_hash(
|
|
35
|
+
SettingsCascade.lookup(instances, instance_key.instance_id),
|
|
36
|
+
path: "provider.instances.#{instance_key.instance_id}"
|
|
37
|
+
)
|
|
38
|
+
tier_weights = scope_hash(
|
|
39
|
+
settings.dig(:llm, :routing, :tier_weights), path: 'llm.routing.tier_weights'
|
|
40
|
+
)
|
|
41
|
+
offerings = scope_hash(SettingsCascade.lookup(provider_conf, :offerings), path: 'provider.offerings')
|
|
42
|
+
offering_w = offering_scope_weight(
|
|
43
|
+
offerings: offerings, instance_key: instance_key, model: model, tier: tier,
|
|
44
|
+
operation_evidence: operation_evidence
|
|
45
|
+
)
|
|
46
|
+
provider_models = scope_hash(
|
|
47
|
+
SettingsCascade.lookup(provider_conf, :models), path: 'provider.models'
|
|
48
|
+
)
|
|
49
|
+
instance_models = scope_hash(
|
|
50
|
+
SettingsCascade.lookup(instance_cfg, :models), path: 'instance.models'
|
|
51
|
+
)
|
|
52
|
+
scope_hash(
|
|
53
|
+
SettingsCascade.lookup(provider_models, model), path: "provider.models.#{model}"
|
|
54
|
+
)
|
|
55
|
+
scope_hash(
|
|
56
|
+
SettingsCascade.lookup(instance_models, model), path: "instance.models.#{model}"
|
|
57
|
+
)
|
|
58
|
+
model_scopes = scope_hash(
|
|
59
|
+
SettingsCascade.merge_model_scopes(
|
|
60
|
+
provider_conf: provider_conf, instance_cfg: instance_cfg, model: model
|
|
61
|
+
),
|
|
62
|
+
path: "merged model scope #{model}"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
model_w = SettingsCascade.lookup(model_scopes, :weight)
|
|
66
|
+
{
|
|
67
|
+
tier: component(SettingsCascade.lookup(tier_weights, tier), IDENTITY),
|
|
68
|
+
provider: component(SettingsCascade.lookup(provider_conf, :weight), IDENTITY),
|
|
69
|
+
instance: component(SettingsCascade.lookup(instance_cfg, :weight), IDENTITY),
|
|
70
|
+
# Explicit nil? precedence (NEVER `||`): offering overrides the model
|
|
71
|
+
# component only; nil offering → model; both nil → identity. A
|
|
72
|
+
# non-nil non-Integer (false, strings, negatives) RAISES in `component`.
|
|
73
|
+
model_or_offering: component(offering_w.nil? ? model_w : offering_w, IDENTITY)
|
|
74
|
+
}
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# The configured offering-scope weight for the draft: the operator keys
|
|
78
|
+
# `extensions.llm.<provider>.offerings` by the lane's 5 tuple. The lanes
|
|
79
|
+
# a draft supports are one 5 tuple per distinct lane type; the draft
|
|
80
|
+
# carries a SINGLE weight pair, so configured weights across its lanes
|
|
81
|
+
# must agree — a disagreement is an ambiguous operator input and RAISES
|
|
82
|
+
# instead of silently picking one.
|
|
83
|
+
def offering_scope_weight(offerings:, instance_key:, model:, tier:, operation_evidence:)
|
|
84
|
+
lane_ids = supported_lane_ids(
|
|
85
|
+
instance_key: instance_key, model: model, tier: tier, operation_evidence: operation_evidence
|
|
86
|
+
)
|
|
87
|
+
weights = lane_ids.filter_map do |lane_id|
|
|
88
|
+
scope = scope_hash(SettingsCascade.lookup(offerings, lane_id), path: "provider.offerings.#{lane_id}")
|
|
89
|
+
SettingsCascade.lookup(scope, :weight)
|
|
90
|
+
end
|
|
91
|
+
return nil if weights.empty?
|
|
92
|
+
|
|
93
|
+
unless weights.uniq.length == 1
|
|
94
|
+
raise ArgumentError, "offering weight scope is ambiguous: the draft's lanes " \
|
|
95
|
+
"#{lane_ids.join(', ')} carry differing configured weights"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
weights.first
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def supported_lane_ids(instance_key:, model:, tier:, operation_evidence:)
|
|
102
|
+
operation_evidence.each_value.with_object([]) do |evidence, lane_ids|
|
|
103
|
+
next unless evidence.supported?
|
|
104
|
+
|
|
105
|
+
lane_id = Identity.compose_lane_id(
|
|
106
|
+
tier: tier, provider_family: instance_key.provider_family,
|
|
107
|
+
instance_id: instance_key.instance_id,
|
|
108
|
+
type: Taxonomies.lane_type_for(operation: evidence.operation), model: model
|
|
109
|
+
)
|
|
110
|
+
lane_ids << lane_id unless lane_ids.include?(lane_id)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# nil → default; an explicit 0 passes through (0 = operator disable —
|
|
115
|
+
# 0 is TRUTHY in Ruby and must not be defaulted); any other non-Integer
|
|
116
|
+
# RAISES instead of silently applying a different weight.
|
|
117
|
+
def component(value, default)
|
|
118
|
+
return default if value.nil?
|
|
119
|
+
|
|
120
|
+
raise ArgumentError, "weight component must be an Integer >= 0, got #{value.inspect}" \
|
|
121
|
+
unless value.is_a?(::Integer) && value >= 0
|
|
122
|
+
|
|
123
|
+
value
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Missing scope is the identity default. A present malformed scope is
|
|
127
|
+
# never treated as missing (`false || {}` would silently erase operator
|
|
128
|
+
# input and recreate a flat identity result).
|
|
129
|
+
def scope_hash(value, path:)
|
|
130
|
+
return {} if value.nil?
|
|
131
|
+
return value if value.is_a?(::Hash)
|
|
132
|
+
|
|
133
|
+
raise ArgumentError, "#{path} must be a Hash, got #{value.inspect}"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Pinned return contract: TWO methods. The writer calls both:
|
|
137
|
+
# wi = WeightSchema.weight_inputs(...)
|
|
138
|
+
# base = WeightSchema.base_weight(wi)
|
|
139
|
+
def base_weight(weight_inputs)
|
|
140
|
+
weight_inputs.values.reduce(1, :*)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|