@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.
Files changed (49) hide show
  1. package/dist/catalog.d.ts +15 -0
  2. package/dist/catalog.d.ts.map +1 -1
  3. package/dist/catalog.js +15 -0
  4. package/dist/cold-tier.d.ts +99 -0
  5. package/dist/cold-tier.d.ts.map +1 -0
  6. package/dist/cold-tier.js +155 -0
  7. package/dist/extensions/local-image-input.d.ts +24 -0
  8. package/dist/extensions/local-image-input.d.ts.map +1 -0
  9. package/dist/extensions/local-image-input.js +114 -0
  10. package/dist/extensions/subagent.d.ts +20 -1
  11. package/dist/extensions/subagent.d.ts.map +1 -1
  12. package/dist/extensions/subagent.js +46 -6
  13. package/dist/paths.d.ts +13 -0
  14. package/dist/paths.d.ts.map +1 -0
  15. package/dist/paths.js +18 -0
  16. package/dist/provider/chat-config.d.ts +1 -1
  17. package/dist/provider/chat-config.d.ts.map +1 -1
  18. package/dist/provider/chat-config.js +15 -3
  19. package/dist/provider/events.d.ts +9 -0
  20. package/dist/provider/events.d.ts.map +1 -1
  21. package/dist/provider/events.js +15 -0
  22. package/dist/provider/index.d.ts +12 -1
  23. package/dist/provider/index.d.ts.map +1 -1
  24. package/dist/provider/index.js +153 -7
  25. package/dist/provider/metrics-trace.d.ts +274 -0
  26. package/dist/provider/metrics-trace.d.ts.map +1 -0
  27. package/dist/provider/metrics-trace.js +174 -0
  28. package/dist/provider/mlx-identity.d.ts +16 -0
  29. package/dist/provider/mlx-identity.d.ts.map +1 -0
  30. package/dist/provider/mlx-identity.js +15 -0
  31. package/dist/provider/model-host.d.ts +45 -3
  32. package/dist/provider/model-host.d.ts.map +1 -1
  33. package/dist/provider/model-host.js +34 -2
  34. package/dist/provider/model-registry-filter.d.ts +74 -18
  35. package/dist/provider/model-registry-filter.d.ts.map +1 -1
  36. package/dist/provider/model-registry-filter.js +229 -38
  37. package/dist/provider/models.d.ts +2 -3
  38. package/dist/provider/models.d.ts.map +1 -1
  39. package/dist/provider/models.js +46 -20
  40. package/dist/provider/stream-adapter.d.ts +37 -4
  41. package/dist/provider/stream-adapter.d.ts.map +1 -1
  42. package/dist/provider/stream-adapter.js +82 -7
  43. package/dist/provider/warm-reuse.d.ts +11 -8
  44. package/dist/provider/warm-reuse.d.ts.map +1 -1
  45. package/dist/provider/warm-reuse.js +14 -7
  46. package/dist/run-agent.d.ts +12 -3
  47. package/dist/run-agent.d.ts.map +1 -1
  48. package/dist/run-agent.js +31 -5
  49. package/package.json +10 -5
@@ -1,72 +1,263 @@
1
1
  /**
2
- * Process-local policy adapter for pi's public ModelRegistry.
2
+ * Process-local policy adapter for pi's canonical `ModelRuntime`.
3
3
  *
4
- * `mlx agent` is an offline/local product, but pi's registry also contains
5
- * every authenticated built-in cloud provider. CLI `--models mlx/*` only sets
6
- * the initial selector scope: Tab, `/models`, RPC enumeration, explicit model
7
- * resolution, and restored sessions still consult the registry's unscoped
8
- * reads. Filter those reads at their shared boundary before pi constructs its
9
- * runtime so every path sees only the exact local models this process serves.
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 ModelRegistry.${name}; expected a writable prototype method`);
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 read policy for one `runAgent()` lifetime.
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(Registry, modelIds) {
28
- const prototype = Registry.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 ModelRegistry filtering in one process is not supported');
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 === 'mlx' && allowedIds.has(model.id) && model.api === 'mlx' && model.baseUrl === 'mlx://local';
34
- const descriptors = {
35
- getAll: requireMethodDescriptor(prototype, 'getAll'),
36
- getAvailable: requireMethodDescriptor(prototype, 'getAvailable'),
37
- find: requireMethodDescriptor(prototype, 'find'),
38
- hasConfiguredAuth: requireMethodDescriptor(prototype, 'hasConfiguredAuth'),
39
- };
40
- const getAll = descriptors.getAll.value;
41
- const getAvailable = descriptors.getAvailable.value;
42
- const find = descriptors.find.value;
43
- const hasConfiguredAuth = descriptors.hasConfiguredAuth.value;
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
- getAll: {
46
- ...descriptors.getAll,
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 getAll.call(this).filter(isAllowed);
126
+ return getAvailableSnapshot.call(this).filter(isAllowed);
49
127
  },
50
128
  },
51
129
  getAvailable: {
52
- ...descriptors.getAvailable,
53
- value() {
54
- return getAvailable.call(this).filter(isAllowed);
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
- find: {
58
- ...descriptors.find,
137
+ getModel: {
138
+ ...originals.getModel,
59
139
  value(provider, modelId) {
60
- if (provider !== 'mlx' || !allowedIds.has(modelId))
140
+ if (provider !== MLX_PROVIDER_ID || !allowedIds.has(modelId))
61
141
  return undefined;
62
- const model = find.call(this, provider, modelId);
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
- ...descriptors.hasConfiguredAuth,
68
- value(model) {
69
- return isAllowed(model) && hasConfiguredAuth.call(this, model);
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, descriptors);
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/cli/src/commands/launch-claude/discover.ts` (which is bin-only
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;;;;;;;;;;;;;;;;GAgBG;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;AAmED;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAsDlF"}
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"}
@@ -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/cli/src/commands/launch-claude/discover.ts` (which is bin-only
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: { reasoning: false, fallbackContextWindow: 131072 },
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 the trained context window from `<modelPath>/config.json`:
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. `detectModelType` already parsed
47
- * this file, so a read/parse failure here (e.g. a racing rewrite) lands
48
- * on the fallback instead of dropping the model.
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 readContextWindow(modelPath, fallback) {
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
- if (typeof textConfig === 'object' && textConfig !== null && !Array.isArray(textConfig)) {
59
- const nested = positiveInteger(textConfig.max_position_embeddings);
60
- if (nested !== undefined)
61
- return nested;
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
- // fall through to the family fallback
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 readContextWindow(full, traits.fallbackContextWindow);
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
- input: ['text'],
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
- /** Atomic resident selection + serialized inference closure (see `MlxModelHost`). */
50
- runWithResident<T>(modelId: string, fn: (session: ChatSession) => Promise<T>): Promise<T>;
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
- export declare function makeMlxStreamSimple(host: StreamSimpleHost, onPerformance?: PerformanceRecorder, resolveRootCacheOwner?: RootCacheOwnerResolver): (model: Model<Api>, context: Context, options?: SimpleStreamOptions) => AssistantMessageEventStream;
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;AAEpE,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,qFAAqF;IACrF,eAAe,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAC1F,+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;AAqF9D,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,gBAAgB,EACtB,aAAa,CAAC,EAAE,mBAAmB,EACnC,qBAAqB,CAAC,EAAE,sBAAsB,GAC7C,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,mBAAmB,KAAK,2BAA2B,CAkOrG"}
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"}