lex-llm-mlx 0.5.2 → 0.5.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/.rubocop.yml +2 -0
- data/CHANGELOG.md +38 -0
- data/Gemfile +7 -2
- data/RULES.md +97 -0
- data/lex-llm-mlx.gemspec +5 -4
- data/lib/legion/extensions/llm/mlx/actors/discovery.rb +24 -0
- data/lib/legion/extensions/llm/mlx/helpers/callable.rb +162 -0
- data/lib/legion/extensions/llm/mlx/provider.rb +0 -141
- data/lib/legion/extensions/llm/mlx/runners/discovery.rb +189 -0
- data/lib/legion/extensions/llm/mlx/runners/fleet_worker.rb +0 -2
- data/lib/legion/extensions/llm/mlx/version.rb +1 -1
- data/lib/legion/extensions/llm/mlx.rb +11 -2
- metadata +7 -4
- data/lib/legion/extensions/llm/mlx/actors/discovery_refresh.rb +0 -864
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'time'
|
|
4
|
+
|
|
5
|
+
require 'legion/extensions/llm/discovery/pipeline'
|
|
6
|
+
require 'legion/extensions/llm/mlx/helpers/callable'
|
|
7
|
+
require 'legion/extensions/llm/mlx/provider'
|
|
8
|
+
|
|
9
|
+
module Legion
|
|
10
|
+
module Extensions
|
|
11
|
+
module Llm
|
|
12
|
+
module Mlx
|
|
13
|
+
module Runners
|
|
14
|
+
# MLX discovery runner: ONLY the MLX-specific work. The generic
|
|
15
|
+
# reconcile / claim / activate / probe (cadence + reactive) / replace /
|
|
16
|
+
# weight-publication / health-display pipeline is mixed in from the
|
|
17
|
+
# shared Discovery::Pipeline. Weight is NOT computed here — the
|
|
18
|
+
# shared WeightReconciler recomputes it from live settings at publish.
|
|
19
|
+
#
|
|
20
|
+
# The catalog is OpenAI-shaped (GET /v1/models -> body[:data]) and
|
|
21
|
+
# readiness is GET /health, so the pipeline's default
|
|
22
|
+
# fetch_raw_models / model_id_from / check_health / health_path are
|
|
23
|
+
# reused. The overrides are the MLX config keys (mlx_api_base /
|
|
24
|
+
# mlx_api_key, defaulting to http://localhost:8000), the
|
|
25
|
+
# Helpers::Callable, and the offering-draft evidence.
|
|
26
|
+
module Discovery
|
|
27
|
+
extend self
|
|
28
|
+
include Legion::Extensions::Llm::Discovery::Pipeline
|
|
29
|
+
|
|
30
|
+
EMBEDDING_PATTERN = /embed|bge|e5|nomic/i
|
|
31
|
+
# Protocol-required evidence source: the default_false taxonomy member.
|
|
32
|
+
UNKNOWN_EVIDENCE_SRC = :default_false
|
|
33
|
+
|
|
34
|
+
# ── MLX instance-config keys / connection ───────────────────────
|
|
35
|
+
# Mlx.normalize_instance_config promotes endpoint/base_url/api_base
|
|
36
|
+
# aliases to :mlx_api_base; a missing base falls back to the
|
|
37
|
+
# extension's registered localhost default.
|
|
38
|
+
def catalog_base_url(instance_cfg:)
|
|
39
|
+
normalize_api_base(instance_cfg[:mlx_api_base] || instance_cfg[:endpoint] || 'http://localhost:8000')
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# MLX is local: auth is an OPTIONAL bearer key (mlx_api_key, or
|
|
43
|
+
# credentials.api_key when no explicit key is set).
|
|
44
|
+
def auth_token(instance_cfg:)
|
|
45
|
+
token = instance_cfg[:mlx_api_key] || instance_cfg.dig(:credentials, :api_key)
|
|
46
|
+
token if token.is_a?(String) && !token.strip.empty?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def build_callable(instance_cfg:)
|
|
50
|
+
Legion::Extensions::Llm::Mlx::Helpers::Callable.new(instance_cfg: instance_cfg, logger: log)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# ── Offering draft (evidence + metadata; NO weight) ───────────────
|
|
54
|
+
def build_offering_draft(instance_cfg:, instance_key:, model_id:, model_data:)
|
|
55
|
+
tier = instance_cfg[:tier] || :local
|
|
56
|
+
embed_supported = embedding_model?(model_id: model_id)
|
|
57
|
+
|
|
58
|
+
Legion::Extensions::Llm::Inventory::OfferingDraft.new(
|
|
59
|
+
provider_native_key: model_id,
|
|
60
|
+
model: model_id,
|
|
61
|
+
tier: tier,
|
|
62
|
+
operation_evidence: build_operation_evidence(embed_supported: embed_supported, model_id: model_id),
|
|
63
|
+
capability_evidence: build_capability_evidence(model_id: model_id),
|
|
64
|
+
context_evidence: build_context_evidence(model_data: model_data),
|
|
65
|
+
max_output_evidence: build_max_output_evidence(model_data: model_data),
|
|
66
|
+
embedding_dimensions_evidence: build_embedding_dimensions_evidence(
|
|
67
|
+
model_data: model_data, embed_supported: embed_supported
|
|
68
|
+
),
|
|
69
|
+
model_revision_evidence: absent_value_evidence,
|
|
70
|
+
tokenizer_evidence: absent_value_evidence,
|
|
71
|
+
quota_domains: {},
|
|
72
|
+
metadata: build_offering_metadata(model_data: model_data, instance_key: instance_key),
|
|
73
|
+
publication_source: :provider_catalog
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def embedding_model?(model_id:)
|
|
80
|
+
model_id.to_s.match?(EMBEDDING_PATTERN)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def build_operation_evidence(embed_supported:, **)
|
|
84
|
+
now = Time.now.freeze
|
|
85
|
+
is_embedding = embed_supported
|
|
86
|
+
{
|
|
87
|
+
chat: op_evidence(operation: :chat, status: is_embedding ? :unsupported : :supported, observed_at: now),
|
|
88
|
+
stream_chat: op_evidence(operation: :stream_chat, status: is_embedding ? :unsupported : :supported,
|
|
89
|
+
observed_at: now),
|
|
90
|
+
embed: op_evidence(operation: :embed, status: is_embedding ? :supported : :unsupported,
|
|
91
|
+
observed_at: now),
|
|
92
|
+
image: op_evidence(operation: :image, status: :unsupported, observed_at: now),
|
|
93
|
+
transcribe: op_evidence(operation: :transcribe, status: :unsupported, observed_at: now),
|
|
94
|
+
translate: op_evidence(operation: :translate, status: :unsupported, observed_at: now),
|
|
95
|
+
speak: op_evidence(operation: :speak, status: :unsupported, observed_at: now),
|
|
96
|
+
moderate: op_evidence(operation: :moderate, status: :unsupported, observed_at: now),
|
|
97
|
+
count_tokens: op_evidence(operation: :count_tokens, status: :unknown, observed_at: now)
|
|
98
|
+
}
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def op_evidence(operation:, status:, observed_at:)
|
|
102
|
+
source = status == :unknown ? UNKNOWN_EVIDENCE_SRC : :provider_implementation
|
|
103
|
+
Legion::Extensions::Llm::Inventory::OperationEvidence.new(
|
|
104
|
+
operation: operation, status: status, source: source, observed_at: observed_at
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def build_capability_evidence(model_id:)
|
|
109
|
+
is_embedding = embedding_model?(model_id: model_id)
|
|
110
|
+
caps = {
|
|
111
|
+
completion: cap_evidence(capability: :completion,
|
|
112
|
+
status: is_embedding ? :unsupported : :supported,
|
|
113
|
+
source: :provider_implementation),
|
|
114
|
+
streaming: cap_evidence(capability: :streaming,
|
|
115
|
+
status: is_embedding ? :unsupported : :supported,
|
|
116
|
+
source: :provider_implementation),
|
|
117
|
+
tools: cap_evidence(capability: :tools, status: :unknown, source: UNKNOWN_EVIDENCE_SRC),
|
|
118
|
+
thinking: cap_evidence(capability: :thinking, status: :unknown, source: UNKNOWN_EVIDENCE_SRC)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if is_embedding
|
|
122
|
+
caps[:embedding] = cap_evidence(
|
|
123
|
+
capability: :embedding, status: :supported, source: :provider_implementation
|
|
124
|
+
)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
caps
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def cap_evidence(capability:, status:, source:)
|
|
131
|
+
Legion::Extensions::Llm::Inventory::CapabilityEvidence.new(
|
|
132
|
+
capability: capability, status: status, source: source, observed_at: Time.now.freeze
|
|
133
|
+
)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def build_context_evidence(model_data:)
|
|
137
|
+
ctx = model_data[:max_model_len] || model_data[:context_length]
|
|
138
|
+
if ctx.is_a?(Integer) && ctx.positive?
|
|
139
|
+
Legion::Extensions::Llm::Inventory::ValueEvidence.new(
|
|
140
|
+
status: :known, value: ctx, source: :provider_catalog
|
|
141
|
+
)
|
|
142
|
+
else
|
|
143
|
+
absent_value_evidence
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def build_max_output_evidence(model_data:)
|
|
148
|
+
max_out = model_data[:max_output_tokens] || model_data[:max_completion_tokens]
|
|
149
|
+
if max_out.is_a?(Integer) && max_out.positive?
|
|
150
|
+
Legion::Extensions::Llm::Inventory::ValueEvidence.new(
|
|
151
|
+
status: :known, value: max_out, source: :provider_catalog
|
|
152
|
+
)
|
|
153
|
+
else
|
|
154
|
+
absent_value_evidence
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def build_embedding_dimensions_evidence(model_data:, embed_supported:)
|
|
159
|
+
unless embed_supported
|
|
160
|
+
return Legion::Extensions::Llm::Inventory::ValueEvidence.new(status: :unknown, source: :absent)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
dims = model_data[:embedding_dimensions]
|
|
164
|
+
if dims.is_a?(Array) && !dims.empty? && dims.all? { |d| d.is_a?(Integer) && d.positive? }
|
|
165
|
+
Legion::Extensions::Llm::Inventory::ValueEvidence.new(
|
|
166
|
+
status: :known, value: dims.uniq.sort, source: :provider_catalog
|
|
167
|
+
)
|
|
168
|
+
else
|
|
169
|
+
absent_value_evidence
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def absent_value_evidence
|
|
174
|
+
Legion::Extensions::Llm::Inventory::ValueEvidence.new(status: :unknown, source: :absent)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def build_offering_metadata(model_data:, instance_key:)
|
|
178
|
+
meta = { raw_model: model_data[:id].to_s }
|
|
179
|
+
meta[:parameter_count] = model_data[:parameter_count] if model_data[:parameter_count]
|
|
180
|
+
meta[:quantization] = model_data[:quantization].to_s if model_data[:quantization]
|
|
181
|
+
meta[:instance_id] = instance_key.instance_id
|
|
182
|
+
meta
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
@@ -34,8 +34,6 @@ module Legion
|
|
|
34
34
|
Legion::Extensions::Llm::Fleet::ProviderResponder.call(
|
|
35
35
|
payload: message,
|
|
36
36
|
provider_family: Mlx::PROVIDER_FAMILY,
|
|
37
|
-
provider_class: Mlx::Provider,
|
|
38
|
-
provider_instances: -> { Mlx.discover_instances },
|
|
39
37
|
registry: Legion::Extensions::Llm::Inventory::Registry,
|
|
40
38
|
delivery: nil,
|
|
41
39
|
properties: nil
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
require 'legion/extensions/llm'
|
|
4
4
|
require 'legion/extensions/llm/mlx/provider'
|
|
5
5
|
require 'legion/extensions/llm/mlx/version'
|
|
6
|
-
require 'legion/extensions/llm/mlx/
|
|
6
|
+
require 'legion/extensions/llm/mlx/helpers/callable'
|
|
7
|
+
require 'legion/extensions/llm/mlx/actors/discovery'
|
|
7
8
|
|
|
8
9
|
module Legion
|
|
9
10
|
module Extensions
|
|
@@ -43,7 +44,15 @@ module Legion
|
|
|
43
44
|
# operator-configured instances. No port-scanning, no fabricated
|
|
44
45
|
# instances, no tier override.
|
|
45
46
|
def self.discover_instances
|
|
46
|
-
configured_instances
|
|
47
|
+
configured_instances.reject do |_, normalized|
|
|
48
|
+
# enabled: false is a skip, not a claimable instance: the shared
|
|
49
|
+
# Discovery::Pipeline reads this method as the single claimable
|
|
50
|
+
# source and would otherwise claim + publish a disabled instance
|
|
51
|
+
# as a live lane (an operator's enabled: false is user-space
|
|
52
|
+
# intent). A credential-less instance (no api base to call) is
|
|
53
|
+
# equally non-claimable — same skip as the vLLM sibling.
|
|
54
|
+
normalized[:enabled] == false || normalized[:mlx_api_base].to_s.strip.empty?
|
|
55
|
+
end
|
|
47
56
|
end
|
|
48
57
|
|
|
49
58
|
# Only instances the operator actually configured are claimable.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lex-llm-mlx
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- LegionIO
|
|
@@ -85,14 +85,14 @@ dependencies:
|
|
|
85
85
|
requirements:
|
|
86
86
|
- - ">="
|
|
87
87
|
- !ruby/object:Gem::Version
|
|
88
|
-
version: 0.
|
|
88
|
+
version: 0.8.0
|
|
89
89
|
type: :runtime
|
|
90
90
|
prerelease: false
|
|
91
91
|
version_requirements: !ruby/object:Gem::Requirement
|
|
92
92
|
requirements:
|
|
93
93
|
- - ">="
|
|
94
94
|
- !ruby/object:Gem::Version
|
|
95
|
-
version: 0.
|
|
95
|
+
version: 0.8.0
|
|
96
96
|
description: MLX provider integration for the LegionIO LLM routing framework.
|
|
97
97
|
email:
|
|
98
98
|
- matthewdiverson@gmail.com
|
|
@@ -109,11 +109,14 @@ files:
|
|
|
109
109
|
- Gemfile
|
|
110
110
|
- LICENSE
|
|
111
111
|
- README.md
|
|
112
|
+
- RULES.md
|
|
112
113
|
- lex-llm-mlx.gemspec
|
|
113
114
|
- lib/legion/extensions/llm/mlx.rb
|
|
114
|
-
- lib/legion/extensions/llm/mlx/actors/
|
|
115
|
+
- lib/legion/extensions/llm/mlx/actors/discovery.rb
|
|
115
116
|
- lib/legion/extensions/llm/mlx/actors/fleet_worker.rb
|
|
117
|
+
- lib/legion/extensions/llm/mlx/helpers/callable.rb
|
|
116
118
|
- lib/legion/extensions/llm/mlx/provider.rb
|
|
119
|
+
- lib/legion/extensions/llm/mlx/runners/discovery.rb
|
|
117
120
|
- lib/legion/extensions/llm/mlx/runners/fleet_worker.rb
|
|
118
121
|
- lib/legion/extensions/llm/mlx/version.rb
|
|
119
122
|
homepage: https://github.com/LegionIO/lex-llm-mlx
|