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.
@@ -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
@@ -4,7 +4,7 @@ module Legion
4
4
  module Extensions
5
5
  module Llm
6
6
  module Mlx
7
- VERSION = '0.5.2'
7
+ VERSION = '0.5.4'
8
8
  end
9
9
  end
10
10
  end
@@ -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/actors/discovery_refresh'
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.2
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.7.1
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.7.1
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/discovery_refresh.rb
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