@mlx-node/agent 0.0.8 → 0.0.10
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.
- package/dist/catalog.d.ts +15 -0
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +15 -0
- package/dist/cold-tier.d.ts +99 -0
- package/dist/cold-tier.d.ts.map +1 -0
- package/dist/cold-tier.js +155 -0
- package/dist/extensions/local-image-input.d.ts +24 -0
- package/dist/extensions/local-image-input.d.ts.map +1 -0
- package/dist/extensions/local-image-input.js +114 -0
- package/dist/extensions/subagent.d.ts +20 -1
- package/dist/extensions/subagent.d.ts.map +1 -1
- package/dist/extensions/subagent.js +46 -6
- package/dist/paths.d.ts +13 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +18 -0
- package/dist/provider/chat-config.d.ts +1 -1
- package/dist/provider/chat-config.d.ts.map +1 -1
- package/dist/provider/chat-config.js +15 -3
- package/dist/provider/events.d.ts +9 -0
- package/dist/provider/events.d.ts.map +1 -1
- package/dist/provider/events.js +15 -0
- package/dist/provider/index.d.ts +12 -1
- package/dist/provider/index.d.ts.map +1 -1
- package/dist/provider/index.js +153 -7
- package/dist/provider/metrics-trace.d.ts +274 -0
- package/dist/provider/metrics-trace.d.ts.map +1 -0
- package/dist/provider/metrics-trace.js +174 -0
- package/dist/provider/mlx-identity.d.ts +16 -0
- package/dist/provider/mlx-identity.d.ts.map +1 -0
- package/dist/provider/mlx-identity.js +15 -0
- package/dist/provider/model-host.d.ts +45 -3
- package/dist/provider/model-host.d.ts.map +1 -1
- package/dist/provider/model-host.js +34 -2
- package/dist/provider/model-registry-filter.d.ts +74 -18
- package/dist/provider/model-registry-filter.d.ts.map +1 -1
- package/dist/provider/model-registry-filter.js +229 -38
- package/dist/provider/models.d.ts +2 -3
- package/dist/provider/models.d.ts.map +1 -1
- package/dist/provider/models.js +46 -20
- package/dist/provider/stream-adapter.d.ts +37 -4
- package/dist/provider/stream-adapter.d.ts.map +1 -1
- package/dist/provider/stream-adapter.js +82 -7
- package/dist/provider/warm-reuse.d.ts +11 -8
- package/dist/provider/warm-reuse.d.ts.map +1 -1
- package/dist/provider/warm-reuse.js +14 -7
- package/dist/run-agent.d.ts +12 -3
- package/dist/run-agent.d.ts.map +1 -1
- package/dist/run-agent.js +31 -5
- package/package.json +10 -5
|
@@ -1,72 +1,263 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Process-local policy adapter for pi's
|
|
2
|
+
* Process-local policy adapter for pi's canonical `ModelRuntime`.
|
|
3
3
|
*
|
|
4
|
-
* `mlx agent` is an offline/local product, but pi's
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* runtime
|
|
4
|
+
* `mlx agent` is an offline/local product, but pi's runtime also composes every
|
|
5
|
+
* built-in cloud provider. CLI `--models mlx/*` only sets the initial selector
|
|
6
|
+
* scope: Tab, `/models`, RPC enumeration, explicit model resolution, and
|
|
7
|
+
* restored sessions all read the runtime's unscoped catalog/availability
|
|
8
|
+
* directly (the `ModelRegistry` facade handed to extensions delegates to the
|
|
9
|
+
* same runtime). Filter those reads at their shared boundary — the runtime
|
|
10
|
+
* prototype — so every path sees only the exact local models this process
|
|
11
|
+
* serves. Patching the runtime (not the extension-only facade) is what keeps
|
|
12
|
+
* the mlx-only guarantee across the selector / listing / resolution paths.
|
|
13
|
+
*
|
|
14
|
+
* The guarantee is an ALLOWLIST across three surfaces, all keyed on the `mlx`
|
|
15
|
+
* provider id:
|
|
16
|
+
* 1. Model reads (`getModels`/`getAvailable*`/`getModel`) — exact local-model
|
|
17
|
+
* identity (`api === 'mlx' && baseUrl === 'mlx://local'`).
|
|
18
|
+
* 2. Provider/auth reads (`getProviders`/`getProvider`/`checkAuth`/`getAuth`/
|
|
19
|
+
* `isUsingOAuth`/`hasConfiguredAuth`/`listCredentials`/`getProviderAuthStatus`)
|
|
20
|
+
* — never surface, report configured, resolve auth for, or enumerate a
|
|
21
|
+
* credential of any non-`mlx` provider. `getAuth` is the pivotal one: pi's
|
|
22
|
+
* `/login`, `/logout`, and the built-in `/llama` command all resolve auth
|
|
23
|
+
* through it, and its OAuth/`LLAMA_BASE_URL` `fetch` consults neither
|
|
24
|
+
* `PI_OFFLINE` nor any allow-network flag; returning `undefined` for non-mlx
|
|
25
|
+
* makes those commands fail before any network or second-model-host load.
|
|
26
|
+
* 3. Auth mutation / network (`login` rejected for non-mlx; `refresh` forced to
|
|
27
|
+
* `allowNetwork: false` so an explicit `allowNetwork: true` — e.g. pi's
|
|
28
|
+
* `update --models` package command — can never override offline mode).
|
|
29
|
+
*
|
|
30
|
+
* The `mlx` provider is registered with a literal apiKey and never needs
|
|
31
|
+
* `/login`; streaming still works because `prepareRequest` calls `getAuth(model)`
|
|
32
|
+
* with `model.provider === 'mlx'`, which passes through.
|
|
33
|
+
*
|
|
34
|
+
* Surfaces 1-3 patch the runtime's PUBLIC facade, but pi's own internals read the
|
|
35
|
+
* composed provider map `this.models` (pi-ai `ModelsImpl`) directly — a boundary
|
|
36
|
+
* the facade patches cannot reach (e.g. `refresh` resolving a command-backed
|
|
37
|
+
* cloud credential). So there is a 4th, structural surface: the `recomposeProvider`
|
|
38
|
+
* choke composes ONLY `mlx` into `this.models`, making every internal read
|
|
39
|
+
* mlx-only by construction. It requires installation before the runtime is
|
|
40
|
+
* constructed (which `runAgent` guarantees).
|
|
10
41
|
*
|
|
11
42
|
* Keep this adapter isolated: once pi exposes a first-class provider allowlist
|
|
12
43
|
* in `MainOptions`, this file can be replaced by that option without touching
|
|
13
44
|
* the provider or CLI layers.
|
|
14
45
|
*/
|
|
46
|
+
import { MLX_API, MLX_API_KEY, MLX_BASE_URL, MLX_PROVIDER_ID } from './mlx-identity.js';
|
|
15
47
|
const activePrototypes = new WeakSet();
|
|
48
|
+
/**
|
|
49
|
+
* `getAuth` is intentionally NOT part of {@link FilterableModelRuntime}: pi types
|
|
50
|
+
* it with two overloads (`getAuth(providerId)` and `getAuth(model)`), which a
|
|
51
|
+
* single structural signature cannot capture without breaking the runtime→
|
|
52
|
+
* interface assignment. It is wrapped by name (mlx pins a fixed resolution; the
|
|
53
|
+
* original is never delegated to).
|
|
54
|
+
*/
|
|
55
|
+
/** Method names wrapped through the structural interface, plus the loose `getAuth`. */
|
|
56
|
+
const STRUCTURAL_METHODS = [
|
|
57
|
+
'getModels',
|
|
58
|
+
'getAvailableSnapshot',
|
|
59
|
+
'getAvailable',
|
|
60
|
+
'getModel',
|
|
61
|
+
'getProvider',
|
|
62
|
+
'hasConfiguredAuth',
|
|
63
|
+
'checkAuth',
|
|
64
|
+
'isUsingOAuth',
|
|
65
|
+
'getProviders',
|
|
66
|
+
'listCredentials',
|
|
67
|
+
'getProviderAuthStatus',
|
|
68
|
+
'login',
|
|
69
|
+
'refresh',
|
|
70
|
+
];
|
|
16
71
|
function requireMethodDescriptor(prototype, name) {
|
|
17
72
|
const descriptor = Object.getOwnPropertyDescriptor(prototype, name);
|
|
18
73
|
if (!descriptor || typeof descriptor.value !== 'function' || descriptor.writable !== true) {
|
|
19
|
-
throw new Error(`mlx agent: incompatible pi
|
|
74
|
+
throw new Error(`mlx agent: incompatible pi ModelRuntime.${name}; expected a writable prototype method`);
|
|
20
75
|
}
|
|
21
76
|
return descriptor;
|
|
22
77
|
}
|
|
23
78
|
/**
|
|
24
|
-
* Install an exact local-model
|
|
25
|
-
* Returns an idempotent restore callback.
|
|
79
|
+
* Install an exact local-model / mlx-only-provider policy for one `runAgent()`
|
|
80
|
+
* lifetime. Returns an idempotent restore callback.
|
|
26
81
|
*/
|
|
27
|
-
export function installMlxOnlyModelRegistryFilter(
|
|
28
|
-
const prototype =
|
|
82
|
+
export function installMlxOnlyModelRegistryFilter(Runtime, modelIds) {
|
|
83
|
+
const prototype = Runtime.prototype;
|
|
29
84
|
if (activePrototypes.has(prototype)) {
|
|
30
|
-
throw new Error('mlx agent: concurrent
|
|
85
|
+
throw new Error('mlx agent: concurrent ModelRuntime filtering in one process is not supported');
|
|
31
86
|
}
|
|
32
87
|
const allowedIds = new Set(modelIds);
|
|
33
|
-
const isAllowed = (model) => model.provider ===
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
};
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
88
|
+
const isAllowed = (model) => model.provider === MLX_PROVIDER_ID &&
|
|
89
|
+
allowedIds.has(model.id) &&
|
|
90
|
+
model.api === MLX_API &&
|
|
91
|
+
model.baseUrl === MLX_BASE_URL;
|
|
92
|
+
// Capture every original descriptor up front (fail-closed if pi's method shape
|
|
93
|
+
// changed), so restore can put them all back verbatim.
|
|
94
|
+
const originals = {};
|
|
95
|
+
for (const name of STRUCTURAL_METHODS)
|
|
96
|
+
originals[name] = requireMethodDescriptor(prototype, name);
|
|
97
|
+
originals.getAuth = requireMethodDescriptor(prototype, 'getAuth');
|
|
98
|
+
// `recomposeProvider` is a private funnel method; capture it by name (it is not
|
|
99
|
+
// part of the structural interface — see the choke wrapper below).
|
|
100
|
+
originals.recomposeProvider = requireMethodDescriptor(prototype, 'recomposeProvider');
|
|
101
|
+
const iface = originals;
|
|
102
|
+
const getModels = iface.getModels.value;
|
|
103
|
+
const getAvailableSnapshot = iface.getAvailableSnapshot.value;
|
|
104
|
+
const getAvailable = iface.getAvailable.value;
|
|
105
|
+
const getModel = iface.getModel.value;
|
|
106
|
+
const getProvider = iface.getProvider.value;
|
|
107
|
+
const hasConfiguredAuth = iface.hasConfiguredAuth.value;
|
|
108
|
+
const checkAuth = iface.checkAuth.value;
|
|
109
|
+
const isUsingOAuth = iface.isUsingOAuth.value;
|
|
110
|
+
const getProviders = iface.getProviders.value;
|
|
111
|
+
const listCredentials = iface.listCredentials.value;
|
|
112
|
+
const getProviderAuthStatus = iface.getProviderAuthStatus.value;
|
|
113
|
+
const login = iface.login.value;
|
|
114
|
+
const refresh = iface.refresh.value;
|
|
115
|
+
const recomposeProvider = originals.recomposeProvider.value;
|
|
44
116
|
Object.defineProperties(prototype, {
|
|
45
|
-
|
|
46
|
-
...
|
|
117
|
+
getModels: {
|
|
118
|
+
...originals.getModels,
|
|
119
|
+
value(providerId) {
|
|
120
|
+
return getModels.call(this, providerId).filter(isAllowed);
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
getAvailableSnapshot: {
|
|
124
|
+
...originals.getAvailableSnapshot,
|
|
47
125
|
value() {
|
|
48
|
-
return
|
|
126
|
+
return getAvailableSnapshot.call(this).filter(isAllowed);
|
|
49
127
|
},
|
|
50
128
|
},
|
|
51
129
|
getAvailable: {
|
|
52
|
-
...
|
|
53
|
-
|
|
54
|
-
|
|
130
|
+
...originals.getAvailable,
|
|
131
|
+
// The runtime read is async, so filter the resolved snapshot. Preserve the
|
|
132
|
+
// Promise contract (never turn a rejection into a filtered success).
|
|
133
|
+
value(providerId) {
|
|
134
|
+
return getAvailable.call(this, providerId).then((models) => models.filter(isAllowed));
|
|
55
135
|
},
|
|
56
136
|
},
|
|
57
|
-
|
|
58
|
-
...
|
|
137
|
+
getModel: {
|
|
138
|
+
...originals.getModel,
|
|
59
139
|
value(provider, modelId) {
|
|
60
|
-
if (provider !==
|
|
140
|
+
if (provider !== MLX_PROVIDER_ID || !allowedIds.has(modelId))
|
|
61
141
|
return undefined;
|
|
62
|
-
const model =
|
|
142
|
+
const model = getModel.call(this, provider, modelId);
|
|
63
143
|
return model && isAllowed(model) ? model : undefined;
|
|
64
144
|
},
|
|
65
145
|
},
|
|
146
|
+
getProvider: {
|
|
147
|
+
...originals.getProvider,
|
|
148
|
+
// Streaming uses `this.models.getProvider` (a different object); this only
|
|
149
|
+
// gates external reads (e.g. `/logout`). Non-mlx must never surface.
|
|
150
|
+
value(providerId) {
|
|
151
|
+
return providerId === MLX_PROVIDER_ID ? getProvider.call(this, providerId) : undefined;
|
|
152
|
+
},
|
|
153
|
+
},
|
|
66
154
|
hasConfiguredAuth: {
|
|
67
|
-
...
|
|
68
|
-
|
|
69
|
-
|
|
155
|
+
...originals.hasConfiguredAuth,
|
|
156
|
+
// The runtime signature takes a providerId string (not a model), so gate on
|
|
157
|
+
// the provider id alone: only 'mlx' may ever report configured auth.
|
|
158
|
+
value(providerId) {
|
|
159
|
+
return providerId === MLX_PROVIDER_ID && hasConfiguredAuth.call(this, providerId);
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
checkAuth: {
|
|
163
|
+
...originals.checkAuth,
|
|
164
|
+
value(providerId) {
|
|
165
|
+
return providerId === MLX_PROVIDER_ID ? checkAuth.call(this, providerId) : Promise.resolve(undefined);
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
isUsingOAuth: {
|
|
169
|
+
...originals.isUsingOAuth,
|
|
170
|
+
value(providerId) {
|
|
171
|
+
return providerId === MLX_PROVIDER_ID && isUsingOAuth.call(this, providerId);
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
getProviders: {
|
|
175
|
+
...originals.getProviders,
|
|
176
|
+
// `/login` enumerates from here; hide every cloud provider so only mlx is
|
|
177
|
+
// ever offered for sign-in.
|
|
178
|
+
value() {
|
|
179
|
+
return getProviders.call(this).filter((provider) => provider.id === MLX_PROVIDER_ID);
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
listCredentials: {
|
|
183
|
+
...originals.listCredentials,
|
|
184
|
+
// `/logout` enumerates stored credentials from here (bypassing the composed
|
|
185
|
+
// model map); never reveal a non-mlx credential.
|
|
186
|
+
value() {
|
|
187
|
+
return listCredentials.call(this).then((creds) => creds.filter((cred) => cred.providerId === MLX_PROVIDER_ID));
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
getProviderAuthStatus: {
|
|
191
|
+
...originals.getProviderAuthStatus,
|
|
192
|
+
// Reads the raw credential/config layer (not the model map); only mlx may
|
|
193
|
+
// report configured.
|
|
194
|
+
value(providerId) {
|
|
195
|
+
return providerId === MLX_PROVIDER_ID ? getProviderAuthStatus.call(this, providerId) : { configured: false };
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
getAuth: {
|
|
199
|
+
...originals.getAuth,
|
|
200
|
+
// Pivotal offline gate + reserved-id invariant. `/login`, `/logout`, and the
|
|
201
|
+
// built-in `/llama` command resolve auth through the runtime's `getAuth`,
|
|
202
|
+
// whose OAuth / `LLAMA_BASE_URL` fetch ignores PI_OFFLINE — so non-mlx must
|
|
203
|
+
// resolve to nothing (string form `getAuth(id)` or model form via
|
|
204
|
+
// `model.provider`). For mlx we do NOT delegate to the composed provider:
|
|
205
|
+
// a persisted `models.json` `{ oauth:'radius', baseUrl }` overlay under the
|
|
206
|
+
// `mlx` id promotes a Radius builtin that merges a radius OAuth method into
|
|
207
|
+
// the composed `mlx` auth; with a stored/expired `mlx` oauth credential the
|
|
208
|
+
// real resolution would trigger an offline OAuth refresh throw ("OAuth
|
|
209
|
+
// refresh failed for mlx") and never reach our local stream. Pin mlx to the
|
|
210
|
+
// fixed local credential instead — identical to the no-overlay case, but
|
|
211
|
+
// immune to the overlay. prepareRequest reads only auth.apiKey/baseUrl(/headers)
|
|
212
|
+
// and dispatches streamSimple on the (api-matched) local closure.
|
|
213
|
+
value(providerOrModel, _overrides) {
|
|
214
|
+
const providerId = typeof providerOrModel === 'string'
|
|
215
|
+
? providerOrModel
|
|
216
|
+
: providerOrModel?.provider;
|
|
217
|
+
if (providerId !== MLX_PROVIDER_ID)
|
|
218
|
+
return Promise.resolve(undefined);
|
|
219
|
+
return Promise.resolve({ auth: { apiKey: MLX_API_KEY, baseUrl: MLX_BASE_URL } });
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
login: {
|
|
223
|
+
...originals.login,
|
|
224
|
+
// Authoritative offline gate: reject any non-mlx login BEFORE dispatch, so
|
|
225
|
+
// the cloud OAuth `fetch` (e.g. radius.pi.dev) can never fire — even if some
|
|
226
|
+
// path passes a provider id directly, bypassing the filtered enumeration.
|
|
227
|
+
value(providerId, type, interaction) {
|
|
228
|
+
if (providerId !== MLX_PROVIDER_ID) {
|
|
229
|
+
return Promise.reject(new Error('mlx agent is offline: provider login is disabled'));
|
|
230
|
+
}
|
|
231
|
+
return login.call(this, providerId, type, interaction);
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
refresh: {
|
|
235
|
+
...originals.refresh,
|
|
236
|
+
// Force offline: pi's `refresh({ allowNetwork })` honours an explicit
|
|
237
|
+
// `true` (e.g. the `update --models` package command) even under
|
|
238
|
+
// PI_OFFLINE=1. Pin it off so no catalog fetch can escape the boundary.
|
|
239
|
+
value(options = {}) {
|
|
240
|
+
return refresh.call(this, { ...options, allowNetwork: false });
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
recomposeProvider: {
|
|
244
|
+
...originals.recomposeProvider,
|
|
245
|
+
// CHOKE POINT (structural backbone). `recomposeProvider` is the single
|
|
246
|
+
// funnel that writes a provider into the runtime's model map `this.models`
|
|
247
|
+
// (pi's `rebuildProviders` sweep of builtins/config + the extension
|
|
248
|
+
// `registerProvider`/`registerNativeProvider`/`unregisterProvider` paths).
|
|
249
|
+
// Compose ONLY `mlx`, so `this.models` is mlx-only by construction. That
|
|
250
|
+
// closes the whole class of INTERNAL `this.models.*` reads the public gates
|
|
251
|
+
// above cannot reach — most importantly `refresh`'s availability pass, which
|
|
252
|
+
// resolves a configured cloud provider's credential (executing a
|
|
253
|
+
// command-backed apiKey) with no allowNetwork gate — plus the `/llama`
|
|
254
|
+
// getAuth and catalog-refresh paths. Requires install before the runtime is
|
|
255
|
+
// created (runAgent installs before pi.main constructs it); the mlx provider
|
|
256
|
+
// registers later via its own `recomposeProvider('mlx')`, allowed through.
|
|
257
|
+
value(providerId) {
|
|
258
|
+
if (providerId !== MLX_PROVIDER_ID)
|
|
259
|
+
return;
|
|
260
|
+
recomposeProvider.call(this, providerId);
|
|
70
261
|
},
|
|
71
262
|
},
|
|
72
263
|
});
|
|
@@ -75,7 +266,7 @@ export function installMlxOnlyModelRegistryFilter(Registry, modelIds) {
|
|
|
75
266
|
return () => {
|
|
76
267
|
if (restored)
|
|
77
268
|
return;
|
|
78
|
-
Object.defineProperties(prototype,
|
|
269
|
+
Object.defineProperties(prototype, originals);
|
|
79
270
|
activePrototypes.delete(prototype);
|
|
80
271
|
restored = true;
|
|
81
272
|
};
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Local model discovery for the mlx pi provider.
|
|
3
3
|
*
|
|
4
|
-
* Ports the discovery walk from
|
|
5
|
-
* `packages/
|
|
6
|
-
* and must not be imported from here; the cli copy stays untouched) and
|
|
4
|
+
* Ports the discovery walk from `@mlx-node/server/host`
|
|
5
|
+
* (`packages/server/src/host/discover.ts`; that copy stays untouched) and
|
|
7
6
|
* pairs every discovered checkpoint with a pi `ProviderModelConfig` entry
|
|
8
7
|
* ready for `pi.registerProvider('mlx', { models })`.
|
|
9
8
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../../src/provider/models.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../../src/provider/models.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAMH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AAG3E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAGvD,6EAA6E;AAC7E,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,mBAAmB,CAAC;IAChC,OAAO,EAAE,mBAAmB,CAAC;CAC9B;AAmHD;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAuDlF"}
|
package/dist/provider/models.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Local model discovery for the mlx pi provider.
|
|
3
3
|
*
|
|
4
|
-
* Ports the discovery walk from
|
|
5
|
-
* `packages/
|
|
6
|
-
* and must not be imported from here; the cli copy stays untouched) and
|
|
4
|
+
* Ports the discovery walk from `@mlx-node/server/host`
|
|
5
|
+
* (`packages/server/src/host/discover.ts`; that copy stays untouched) and
|
|
7
6
|
* pairs every discovered checkpoint with a pi `ProviderModelConfig` entry
|
|
8
7
|
* ready for `pi.registerProvider('mlx', { models })`.
|
|
9
8
|
*
|
|
@@ -32,39 +31,65 @@ const FAMILY_TRAITS = {
|
|
|
32
31
|
qwen3: { reasoning: true, fallbackContextWindow: 40960 },
|
|
33
32
|
qwen3_5: { reasoning: true, fallbackContextWindow: 262144 },
|
|
34
33
|
qwen3_5_moe: { reasoning: true, fallbackContextWindow: 262144 },
|
|
35
|
-
gemma4: {
|
|
34
|
+
gemma4: {
|
|
35
|
+
reasoning: true,
|
|
36
|
+
thinkingLevelMap: {
|
|
37
|
+
minimal: 'minimal',
|
|
38
|
+
low: null,
|
|
39
|
+
medium: null,
|
|
40
|
+
high: 'high',
|
|
41
|
+
},
|
|
42
|
+
fallbackContextWindow: 131072,
|
|
43
|
+
},
|
|
36
44
|
lfm2: { reasoning: true, fallbackContextWindow: 128000 },
|
|
37
45
|
lfm2_moe: { reasoning: true, fallbackContextWindow: 128000 },
|
|
38
46
|
};
|
|
39
47
|
function positiveInteger(value) {
|
|
40
48
|
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? Math.floor(value) : undefined;
|
|
41
49
|
}
|
|
50
|
+
function nonEmptyRecord(value) {
|
|
51
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value) && Object.keys(value).length > 0;
|
|
52
|
+
}
|
|
42
53
|
/**
|
|
43
|
-
* Read
|
|
54
|
+
* Read cheap discovery metadata from `<modelPath>/config.json`.
|
|
55
|
+
*
|
|
56
|
+
* The trained context window comes from:
|
|
44
57
|
* root `max_position_embeddings` first (qwen3, lfm2), then
|
|
45
58
|
* `text_config.max_position_embeddings` (qwen3_5, qwen3_5_moe, gemma4
|
|
46
|
-
* unified), else the family fallback.
|
|
47
|
-
*
|
|
48
|
-
*
|
|
59
|
+
* unified), else the family fallback.
|
|
60
|
+
*
|
|
61
|
+
* Image support is advertised only when a family with a native multimodal
|
|
62
|
+
* implementation carries its valid, non-empty vision marker: `vision_config`
|
|
63
|
+
* for Qwen, and either `vision_config` or `unified_vision_config` for Gemma.
|
|
64
|
+
* This lets Pi's model picker and `--list-models` expose checkpoint capability
|
|
65
|
+
* without loading weights. The first resident load remains authoritative and
|
|
66
|
+
* reconciles this optimistic config-level advertisement via
|
|
67
|
+
* `session.supportsImages()` (for example, when conversion stripped an
|
|
68
|
+
* incompatible vision tower).
|
|
69
|
+
*
|
|
70
|
+
* `detectModelType` already parsed this file, so a read/parse failure here
|
|
71
|
+
* (e.g. a racing rewrite) lands on the context fallback and text-only input
|
|
72
|
+
* instead of dropping the model or guessing a positive capability.
|
|
49
73
|
*/
|
|
50
|
-
async function
|
|
74
|
+
async function readDiscoveryMetadata(modelPath, modelType, fallbackContextWindow) {
|
|
51
75
|
try {
|
|
52
76
|
const raw = await readFile(join(modelPath, 'config.json'), 'utf-8');
|
|
53
77
|
const config = JSON.parse(raw);
|
|
54
78
|
const root = positiveInteger(config.max_position_embeddings);
|
|
55
|
-
if (root !== undefined)
|
|
56
|
-
return root;
|
|
57
79
|
const textConfig = config.text_config;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
80
|
+
const nested = nonEmptyRecord(textConfig) ? positiveInteger(textConfig.max_position_embeddings) : undefined;
|
|
81
|
+
const hasVisionConfig = nonEmptyRecord(config.vision_config);
|
|
82
|
+
const supportsImages = modelType === 'gemma4'
|
|
83
|
+
? hasVisionConfig || nonEmptyRecord(config.unified_vision_config)
|
|
84
|
+
: (modelType === 'qwen3_5' || modelType === 'qwen3_5_moe') && hasVisionConfig;
|
|
85
|
+
return {
|
|
86
|
+
contextWindow: root ?? nested ?? fallbackContextWindow,
|
|
87
|
+
supportsImages,
|
|
88
|
+
};
|
|
63
89
|
}
|
|
64
90
|
catch {
|
|
65
|
-
|
|
91
|
+
return { contextWindow: fallbackContextWindow, supportsImages: false };
|
|
66
92
|
}
|
|
67
|
-
return fallback;
|
|
68
93
|
}
|
|
69
94
|
/**
|
|
70
95
|
* Scan `modelsDir` for chat-capable model subdirectories and build their
|
|
@@ -113,14 +138,15 @@ export async function discoverMlxModels(modelsDir) {
|
|
|
113
138
|
continue;
|
|
114
139
|
}
|
|
115
140
|
const name = basename(full);
|
|
116
|
-
const contextWindow = await
|
|
141
|
+
const { contextWindow, supportsImages } = await readDiscoveryMetadata(full, modelType, traits.fallbackContextWindow);
|
|
117
142
|
out.push({
|
|
118
143
|
discovered: { name, path: full, modelType },
|
|
119
144
|
piModel: {
|
|
120
145
|
id: name,
|
|
121
146
|
name,
|
|
122
147
|
reasoning: traits.reasoning,
|
|
123
|
-
|
|
148
|
+
thinkingLevelMap: traits.thinkingLevelMap,
|
|
149
|
+
input: supportsImages ? ['text', 'image'] : ['text'],
|
|
124
150
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
125
151
|
contextWindow,
|
|
126
152
|
maxTokens: preset.maxOutputTokens,
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
* layer is swallowed: there is no further recovery surface.
|
|
37
37
|
*/
|
|
38
38
|
import type { Api, AssistantMessage, AssistantMessageEventStream, Context, Model, SimpleStreamOptions } from '@earendil-works/pi-ai';
|
|
39
|
-
import type { ChatSession, PerformanceMetrics } from '@mlx-node/lm';
|
|
39
|
+
import type { ChatSession, ChatStreamFinal, PerformanceMetrics } from '@mlx-node/lm';
|
|
40
40
|
import type { DiscoveredModelLike } from '../types.js';
|
|
41
41
|
/**
|
|
42
42
|
* The exact `MlxModelHost` surface the adapter consumes, kept structural
|
|
@@ -46,8 +46,12 @@ import type { DiscoveredModelLike } from '../types.js';
|
|
|
46
46
|
export interface StreamSimpleHost {
|
|
47
47
|
/** Discovery record for `modelId` (source of the `ModelType` → launch preset). */
|
|
48
48
|
modelInfo(modelId: string): DiscoveredModelLike | undefined;
|
|
49
|
-
/**
|
|
50
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Atomic resident selection + serialized inference closure (see `MlxModelHost`).
|
|
51
|
+
* `fn` receives a `resident` boolean: `true` when the model was already warm
|
|
52
|
+
* (reused), `false` when this turn had to load/swap it.
|
|
53
|
+
*/
|
|
54
|
+
runWithResident<T>(modelId: string, fn: (session: ChatSession, resident: boolean) => Promise<T>): Promise<T>;
|
|
51
55
|
/** Flag the resident as post-error so the next turn does a full reset (see `MlxModelHost`). */
|
|
52
56
|
markResidentDirty(modelId: string): void;
|
|
53
57
|
/** Read-and-clear the resident's post-error flag; `true` ⇒ full-reset this turn. */
|
|
@@ -57,5 +61,34 @@ export interface StreamSimpleHost {
|
|
|
57
61
|
}
|
|
58
62
|
export type PerformanceRecorder = (message: AssistantMessage, performance: PerformanceMetrics) => void;
|
|
59
63
|
export type RootCacheOwnerResolver = () => string | undefined;
|
|
60
|
-
|
|
64
|
+
/** Resolves the root session's JSONL path for the metrics-trace root correlation. */
|
|
65
|
+
export type RootSessionFileResolver = () => string | undefined;
|
|
66
|
+
/**
|
|
67
|
+
* Durable per-turn telemetry hook. Fires exactly once, only on a SUCCESSFUL
|
|
68
|
+
* native final (never on abort / error / load failure), from the one seam
|
|
69
|
+
* that sees the raw `ChatStreamFinal` alongside the per-request
|
|
70
|
+
* `options.sessionId` and the turn's minted `traceId`. Best-effort: the
|
|
71
|
+
* adapter guards the call so a throwing recorder can never break inference.
|
|
72
|
+
*/
|
|
73
|
+
export type TurnRecorder = (rec: {
|
|
74
|
+
traceId: string;
|
|
75
|
+
sessionId?: string;
|
|
76
|
+
/** Root session id/file snapshotted when this turn was submitted (see below). */
|
|
77
|
+
rootSessionId?: string;
|
|
78
|
+
rootSessionFile?: string;
|
|
79
|
+
model: string;
|
|
80
|
+
final: ChatStreamFinal;
|
|
81
|
+
/** Whole-turn wall-clock (ms): queue wait + resident selection + prefill + decode. */
|
|
82
|
+
durationMs: number;
|
|
83
|
+
/**
|
|
84
|
+
* Queue + cold-load wait (ms) BEFORE native work began this turn: the gap
|
|
85
|
+
* between turn submission and the serialized `runWithResident` callback
|
|
86
|
+
* firing (behind earlier inference and/or a model load/swap). Subtract from
|
|
87
|
+
* `durationMs` to recover execution-only time.
|
|
88
|
+
*/
|
|
89
|
+
queueMs: number;
|
|
90
|
+
/** `true` when the model was already warm/resident, `false` on a cold load/swap this turn. */
|
|
91
|
+
resident: boolean;
|
|
92
|
+
}) => void;
|
|
93
|
+
export declare function makeMlxStreamSimple(host: StreamSimpleHost, onPerformance?: PerformanceRecorder, resolveRootCacheOwner?: RootCacheOwnerResolver, onTurnRecord?: TurnRecorder, onTurnStart?: () => void, resolveRootSessionFile?: RootSessionFileResolver): (model: Model<Api>, context: Context, options?: SimpleStreamOptions) => AssistantMessageEventStream;
|
|
61
94
|
//# sourceMappingURL=stream-adapter.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stream-adapter.d.ts","sourceRoot":"","sources":["../../src/provider/stream-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,KAAK,EACV,GAAG,EACH,gBAAgB,EAChB,2BAA2B,EAC3B,OAAO,EACP,KAAK,EACL,mBAAmB,EACpB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,KAAK,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"stream-adapter.d.ts","sourceRoot":"","sources":["../../src/provider/stream-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,KAAK,EACV,GAAG,EACH,gBAAgB,EAChB,2BAA2B,EAC3B,OAAO,EACP,KAAK,EACL,mBAAmB,EACpB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAErF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAOvD;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,kFAAkF;IAClF,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS,CAAC;IAC5D;;;;OAIG;IACH,eAAe,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAC7G,+FAA+F;IAC/F,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACzC,oFAAoF;IACpF,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/C,gFAAgF;IAChF,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3C;AAED,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,kBAAkB,KAAK,IAAI,CAAC;AACvG,MAAM,MAAM,sBAAsB,GAAG,MAAM,MAAM,GAAG,SAAS,CAAC;AAC9D,qFAAqF;AACrF,MAAM,MAAM,uBAAuB,GAAG,MAAM,MAAM,GAAG,SAAS,CAAC;AAE/D;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,GAAG,EAAE;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iFAAiF;IACjF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,eAAe,CAAC;IACvB,sFAAsF;IACtF,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB,8FAA8F;IAC9F,QAAQ,EAAE,OAAO,CAAC;CACnB,KAAK,IAAI,CAAC;AAwGX,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,gBAAgB,EACtB,aAAa,CAAC,EAAE,mBAAmB,EACnC,qBAAqB,CAAC,EAAE,sBAAsB,EAC9C,YAAY,CAAC,EAAE,YAAY,EAC3B,WAAW,CAAC,EAAE,MAAM,IAAI,EACxB,sBAAsB,CAAC,EAAE,uBAAuB,GAC/C,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,mBAAmB,KAAK,2BAA2B,CA6RrG"}
|