@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
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `MetricsTrace` — always-on per-turn inference telemetry, appended as JSON
|
|
3
|
+
* Lines to `$HOME/.mlx-node/metrics/traces/<YYYY-MM-DD>-<pid>.jsonl`.
|
|
4
|
+
*
|
|
5
|
+
* This is a durable sink that complements the transient in-memory
|
|
6
|
+
* {@link ./performance-status.ts} WeakMap (which only feeds the live TUI
|
|
7
|
+
* footer). One record is written per successful inference turn so the
|
|
8
|
+
* dashboard can correlate throughput, cache reuse, and cold-tier deltas back
|
|
9
|
+
* to the pi session that produced them via `mlxTraceId`.
|
|
10
|
+
*
|
|
11
|
+
* Contract:
|
|
12
|
+
* - Default-on; the `MLX_AGENT_METRICS` env var set to `0` / `false` / `off`
|
|
13
|
+
* (case-insensitive) is the only kill switch.
|
|
14
|
+
* - `record()` NEVER throws: every field is allowlisted (no free text ever
|
|
15
|
+
* lands on disk) and all fs work is wrapped — telemetry must never break
|
|
16
|
+
* an inference turn.
|
|
17
|
+
*/
|
|
18
|
+
import { appendFileSync, mkdirSync } from 'node:fs';
|
|
19
|
+
import { dirname, join } from 'node:path';
|
|
20
|
+
import { metricsTraceDir } from '../paths.js';
|
|
21
|
+
/**
|
|
22
|
+
* Per-turn delta of every COUNTER on the native `ColdCacheStats` — the paged
|
|
23
|
+
* K/V block traffic. One entry per native counter field, named
|
|
24
|
+
* `cold` + PascalCase(nativeKey); the three non-counter fields (`enabled`,
|
|
25
|
+
* `root`, `quotaBytes`) describe the tier's identity rather than a turn and are
|
|
26
|
+
* carried separately as {@link MetricsTraceRecord.coldEnabled} /
|
|
27
|
+
* {@link MetricsTraceRecord.coldRoot}.
|
|
28
|
+
*
|
|
29
|
+
* This is the TS half of a cross-language invariant.
|
|
30
|
+
* `__test__/cold-counter-fields.test.ts` derives the same names from
|
|
31
|
+
* `coldCacheStats()` at runtime and demands an exact match, so a counter added
|
|
32
|
+
* natively but not here — or, the failure this list exists for, one quietly
|
|
33
|
+
* dropped from here — is a red test rather than an empty dashboard column.
|
|
34
|
+
*/
|
|
35
|
+
export const COLD_COUNTER_FIELDS = [
|
|
36
|
+
'coldHits',
|
|
37
|
+
'coldMisses',
|
|
38
|
+
'coldEnqueued',
|
|
39
|
+
'coldQueueDrops',
|
|
40
|
+
'coldBytesWritten',
|
|
41
|
+
'coldBytesRestored',
|
|
42
|
+
'coldEvictions',
|
|
43
|
+
'coldCorruptions',
|
|
44
|
+
'coldWriteErrors',
|
|
45
|
+
'coldRestoreDeclines',
|
|
46
|
+
];
|
|
47
|
+
/**
|
|
48
|
+
* Per-turn delta of every counter on the native `ColdSidecarStats` — the
|
|
49
|
+
* recurrent / sliding-window state that lives OUTSIDE the paged pool. Named
|
|
50
|
+
* `coldSidecar` + PascalCase(nativeKey).
|
|
51
|
+
*
|
|
52
|
+
* Deliberately a second, differently-prefixed list rather than a merge: both
|
|
53
|
+
* native structs carry `enqueued` and `queueDrops`, and they count different
|
|
54
|
+
* objects (blocks vs sidecars). Flattening them into one namespace would make
|
|
55
|
+
* two unrelated numbers collide on one column.
|
|
56
|
+
*/
|
|
57
|
+
export const COLD_SIDECAR_FIELDS = [
|
|
58
|
+
'coldSidecarCaptureReached',
|
|
59
|
+
'coldSidecarChainEmpty',
|
|
60
|
+
'coldSidecarBoundarySkips',
|
|
61
|
+
'coldSidecarAlreadyPersisted',
|
|
62
|
+
'coldSidecarEnqueued',
|
|
63
|
+
'coldSidecarQueueDrops',
|
|
64
|
+
'coldSidecarInstalled',
|
|
65
|
+
'coldSidecarRestoreSuppressed',
|
|
66
|
+
];
|
|
67
|
+
/** Every cold-tier per-turn delta field, in native-struct order. */
|
|
68
|
+
export const COLD_DELTA_FIELDS = [...COLD_COUNTER_FIELDS, ...COLD_SIDECAR_FIELDS];
|
|
69
|
+
function envDisabled() {
|
|
70
|
+
const raw = process.env.MLX_AGENT_METRICS;
|
|
71
|
+
if (raw === undefined)
|
|
72
|
+
return false;
|
|
73
|
+
const normalized = raw.trim().toLowerCase();
|
|
74
|
+
return normalized === '0' || normalized === 'false' || normalized === 'off';
|
|
75
|
+
}
|
|
76
|
+
/** A finite number, or `undefined` if the value is absent / non-finite. */
|
|
77
|
+
function finite(value) {
|
|
78
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
79
|
+
}
|
|
80
|
+
export class MetricsTrace {
|
|
81
|
+
enabled;
|
|
82
|
+
dir;
|
|
83
|
+
now;
|
|
84
|
+
constructor(opts) {
|
|
85
|
+
this.enabled = !envDisabled();
|
|
86
|
+
this.dir = opts?.dir ?? metricsTraceDir();
|
|
87
|
+
this.now = opts?.now ?? Date.now;
|
|
88
|
+
}
|
|
89
|
+
/** `<dir>/<YYYY-MM-DD>-<pid>.jsonl` — UTC date so rotation is timezone-stable. */
|
|
90
|
+
currentFile() {
|
|
91
|
+
const date = new Date(this.now()).toISOString().slice(0, 10);
|
|
92
|
+
return join(this.dir, `${date}-${process.pid}.jsonl`);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Append one allowlisted JSON line. Never throws: a broken sink must not
|
|
96
|
+
* surface into the inference path. Excess input properties are dropped — the
|
|
97
|
+
* record is rebuilt field by field so free text can never reach disk.
|
|
98
|
+
*/
|
|
99
|
+
record(rec) {
|
|
100
|
+
if (!this.enabled)
|
|
101
|
+
return;
|
|
102
|
+
try {
|
|
103
|
+
const out = {
|
|
104
|
+
v: 1,
|
|
105
|
+
traceId: rec.traceId,
|
|
106
|
+
ts: rec.ts,
|
|
107
|
+
model: rec.model,
|
|
108
|
+
durationMs: rec.durationMs,
|
|
109
|
+
finishReason: rec.finishReason,
|
|
110
|
+
promptTokens: rec.promptTokens,
|
|
111
|
+
cachedTokens: rec.cachedTokens,
|
|
112
|
+
outputTokens: rec.outputTokens,
|
|
113
|
+
reasoningTokens: rec.reasoningTokens,
|
|
114
|
+
};
|
|
115
|
+
if (rec.sessionId !== undefined)
|
|
116
|
+
out.sessionId = rec.sessionId;
|
|
117
|
+
if (rec.rootSessionId !== undefined)
|
|
118
|
+
out.rootSessionId = rec.rootSessionId;
|
|
119
|
+
if (rec.rootSessionFile !== undefined)
|
|
120
|
+
out.rootSessionFile = rec.rootSessionFile;
|
|
121
|
+
const queueMs = finite(rec.queueMs);
|
|
122
|
+
if (queueMs !== undefined)
|
|
123
|
+
out.queueMs = queueMs;
|
|
124
|
+
if (typeof rec.resident === 'boolean')
|
|
125
|
+
out.resident = rec.resident;
|
|
126
|
+
const ttftMs = finite(rec.ttftMs);
|
|
127
|
+
if (ttftMs !== undefined)
|
|
128
|
+
out.ttftMs = ttftMs;
|
|
129
|
+
const prefillTps = finite(rec.prefillTps);
|
|
130
|
+
if (prefillTps !== undefined)
|
|
131
|
+
out.prefillTps = prefillTps;
|
|
132
|
+
const decodeTps = finite(rec.decodeTps);
|
|
133
|
+
if (decodeTps !== undefined)
|
|
134
|
+
out.decodeTps = decodeTps;
|
|
135
|
+
const mtpCycles = finite(rec.mtpCycles);
|
|
136
|
+
if (mtpCycles !== undefined)
|
|
137
|
+
out.mtpCycles = mtpCycles;
|
|
138
|
+
const mtpMeanAccepted = finite(rec.mtpMeanAccepted);
|
|
139
|
+
if (mtpMeanAccepted !== undefined)
|
|
140
|
+
out.mtpMeanAccepted = mtpMeanAccepted;
|
|
141
|
+
// Every cold-tier delta comes off ONE list, so a counter can only be
|
|
142
|
+
// dropped from the JSONL by being dropped from `COLD_DELTA_FIELDS` —
|
|
143
|
+
// which `__test__/cold-counter-fields.test.ts` pins to the native structs.
|
|
144
|
+
// Spelling them out here is what let four `coldCacheStats()` counters sit
|
|
145
|
+
// unwritten for the life of the feature.
|
|
146
|
+
for (const key of COLD_DELTA_FIELDS) {
|
|
147
|
+
const value = finite(rec[key]);
|
|
148
|
+
if (value !== undefined)
|
|
149
|
+
out[key] = value;
|
|
150
|
+
}
|
|
151
|
+
// A cache identity is only meaningful for a tier that was actually open;
|
|
152
|
+
// an empty root would create a bucket no dashboard root can ever match.
|
|
153
|
+
if (typeof rec.coldRoot === 'string' && rec.coldRoot.length > 0)
|
|
154
|
+
out.coldRoot = rec.coldRoot;
|
|
155
|
+
if (typeof rec.coldEnabled === 'boolean')
|
|
156
|
+
out.coldEnabled = rec.coldEnabled;
|
|
157
|
+
const coldCorruptionsTotal = finite(rec.coldCorruptionsTotal);
|
|
158
|
+
if (coldCorruptionsTotal !== undefined)
|
|
159
|
+
out.coldCorruptionsTotal = coldCorruptionsTotal;
|
|
160
|
+
const coldQueueDropsTotal = finite(rec.coldQueueDropsTotal);
|
|
161
|
+
if (coldQueueDropsTotal !== undefined)
|
|
162
|
+
out.coldQueueDropsTotal = coldQueueDropsTotal;
|
|
163
|
+
const coldWriteErrorsTotal = finite(rec.coldWriteErrorsTotal);
|
|
164
|
+
if (coldWriteErrorsTotal !== undefined)
|
|
165
|
+
out.coldWriteErrorsTotal = coldWriteErrorsTotal;
|
|
166
|
+
const file = this.currentFile();
|
|
167
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
168
|
+
appendFileSync(file, `${JSON.stringify(out)}\n`);
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
// Telemetry is best-effort; a broken sink must never break inference.
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity of the single in-process `mlx` provider, shared between the provider
|
|
3
|
+
* registration ({@link ../provider/index.ts}) and the mlx-only policy adapter
|
|
4
|
+
* ({@link ./model-registry-filter.ts}) so the two can never drift. In particular
|
|
5
|
+
* the adapter pins mlx auth to {@link MLX_API_KEY}/{@link MLX_BASE_URL}, which
|
|
6
|
+
* MUST match what `registerProvider('mlx', …)` sets.
|
|
7
|
+
*/
|
|
8
|
+
/** Reserved provider id for the local mlx provider. */
|
|
9
|
+
export declare const MLX_PROVIDER_ID = "mlx";
|
|
10
|
+
/** Provider `api` tag for local mlx models. */
|
|
11
|
+
export declare const MLX_API = "mlx";
|
|
12
|
+
/** Local (non-network) base URL for the mlx provider. */
|
|
13
|
+
export declare const MLX_BASE_URL = "mlx://local";
|
|
14
|
+
/** Literal marker apiKey — flags the provider configured; never a real key. */
|
|
15
|
+
export declare const MLX_API_KEY = "mlx-local";
|
|
16
|
+
//# sourceMappingURL=mlx-identity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mlx-identity.d.ts","sourceRoot":"","sources":["../../src/provider/mlx-identity.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,uDAAuD;AACvD,eAAO,MAAM,eAAe,QAAQ,CAAC;AACrC,+CAA+C;AAC/C,eAAO,MAAM,OAAO,QAAQ,CAAC;AAC7B,yDAAyD;AACzD,eAAO,MAAM,YAAY,gBAAgB,CAAC;AAC1C,+EAA+E;AAC/E,eAAO,MAAM,WAAW,cAAc,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity of the single in-process `mlx` provider, shared between the provider
|
|
3
|
+
* registration ({@link ../provider/index.ts}) and the mlx-only policy adapter
|
|
4
|
+
* ({@link ./model-registry-filter.ts}) so the two can never drift. In particular
|
|
5
|
+
* the adapter pins mlx auth to {@link MLX_API_KEY}/{@link MLX_BASE_URL}, which
|
|
6
|
+
* MUST match what `registerProvider('mlx', …)` sets.
|
|
7
|
+
*/
|
|
8
|
+
/** Reserved provider id for the local mlx provider. */
|
|
9
|
+
export const MLX_PROVIDER_ID = 'mlx';
|
|
10
|
+
/** Provider `api` tag for local mlx models. */
|
|
11
|
+
export const MLX_API = 'mlx';
|
|
12
|
+
/** Local (non-network) base URL for the mlx provider. */
|
|
13
|
+
export const MLX_BASE_URL = 'mlx://local';
|
|
14
|
+
/** Literal marker apiKey — flags the provider configured; never a real key. */
|
|
15
|
+
export const MLX_API_KEY = 'mlx-local';
|
|
@@ -13,16 +13,43 @@
|
|
|
13
13
|
* overlapping native activity on the compiled-path globals).
|
|
14
14
|
*/
|
|
15
15
|
import { ChatSession, loadModel } from '@mlx-node/lm';
|
|
16
|
+
import { COLD_TIER_RESTORE_FAMILIES } from '../cold-tier.js';
|
|
16
17
|
import type { DiscoveredModelLike } from '../types.js';
|
|
18
|
+
/**
|
|
19
|
+
* Re-exported so the HOST consults exactly the symbol the drift guard and the
|
|
20
|
+
* `--no-persist-cache` help text consult. The definition lives in the
|
|
21
|
+
* native-free `../cold-tier.js` leaf (reachable off-package through the
|
|
22
|
+
* `@mlx-node/agent/catalog` subpath, which re-exports it)
|
|
23
|
+
* because this module value-imports `@mlx-node/lm`, which loads the native
|
|
24
|
+
* addon — the dashboard and the CLI help path must be able to read the list
|
|
25
|
+
* without that.
|
|
26
|
+
*/
|
|
27
|
+
export { COLD_TIER_RESTORE_FAMILIES };
|
|
28
|
+
/** Per-load policy handed to {@link MlxModelHostOptions.resolveModelPathFn}. */
|
|
29
|
+
export interface ModelLoadPolicy {
|
|
30
|
+
/**
|
|
31
|
+
* Authoritative cold-tier directive for the config overlay
|
|
32
|
+
* (`persist_paged_cache` in the cloned config.json). Present ONLY for loads
|
|
33
|
+
* of a {@link COLD_TIER_RESTORE_FAMILIES} family, carrying the resolved
|
|
34
|
+
* {@link MlxModelHostOptions.persistPagedCache} value as an EXPLICIT
|
|
35
|
+
* boolean: `true` enables the SSD cold tier, `false` authoritatively
|
|
36
|
+
* disables it — overriding any `persist_paged_cache` the checkpoint's own
|
|
37
|
+
* config.json hard-codes, so `mlx agent --no-persist-cache` truly wins.
|
|
38
|
+
* Every other family receives no policy at all, so the overlay never touches
|
|
39
|
+
* the field for them.
|
|
40
|
+
*/
|
|
41
|
+
persistPagedCache: boolean;
|
|
42
|
+
}
|
|
17
43
|
export interface MlxModelHostOptions {
|
|
18
44
|
/** Injectable model loader so tests can stub native loading. */
|
|
19
45
|
loadModelFn?: typeof loadModel;
|
|
20
46
|
/**
|
|
21
47
|
* Optional load-path policy. `mlx agent` uses this to point the loader at
|
|
22
48
|
* an ephemeral config overlay with block-paged attention enabled while
|
|
23
|
-
* leaving the checkpoint directory untouched.
|
|
49
|
+
* leaving the checkpoint directory untouched. The optional per-load policy
|
|
50
|
+
* carries the qwen3 cold-tier opt-in (see {@link ModelLoadPolicy}).
|
|
24
51
|
*/
|
|
25
|
-
resolveModelPathFn?: (model: DiscoveredModelLike) => Promise<string>;
|
|
52
|
+
resolveModelPathFn?: (model: DiscoveredModelLike, policy?: ModelLoadPolicy) => Promise<string>;
|
|
26
53
|
/**
|
|
27
54
|
* Reject a loaded model unless its native paged-cache adapter is active.
|
|
28
55
|
* The agent entrypoint enables this so a model/platform incompatibility
|
|
@@ -32,12 +59,21 @@ export interface MlxModelHostOptions {
|
|
|
32
59
|
* executor is currently flat-cache-only.
|
|
33
60
|
*/
|
|
34
61
|
requirePagedCache?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Enable the cold tier (persisted paged prefix blocks) by default. `mlx
|
|
64
|
+
* agent` turns this on; `mlx agent --no-persist-cache` sets it false.
|
|
65
|
+
* Applied only to loads of a {@link COLD_TIER_RESTORE_FAMILIES} family —
|
|
66
|
+
* every other family keeps per-layer state outside the paged pool, so its
|
|
67
|
+
* prefix cannot be restored soundly. Defaults true.
|
|
68
|
+
*/
|
|
69
|
+
persistPagedCache?: boolean;
|
|
35
70
|
}
|
|
36
71
|
export declare class MlxModelHost {
|
|
37
72
|
private readonly byName;
|
|
38
73
|
private readonly loadModelFn;
|
|
39
74
|
private readonly resolveModelPathFn;
|
|
40
75
|
private readonly requirePagedCache;
|
|
76
|
+
private readonly persistPagedCache;
|
|
41
77
|
private resident;
|
|
42
78
|
private chain;
|
|
43
79
|
constructor(models: DiscoveredModelLike[], opts?: MlxModelHostOptions);
|
|
@@ -57,13 +93,19 @@ export declare class MlxModelHost {
|
|
|
57
93
|
* use the resident session; there is deliberately no method that
|
|
58
94
|
* returns a session outside the serialized section.
|
|
59
95
|
*
|
|
96
|
+
* `fn` also receives a `resident` boolean: `true` when the turn reused
|
|
97
|
+
* the already-loaded model (warm — no load happened), `false` when it had
|
|
98
|
+
* to load or swap the checkpoint first. This is the same warm/cold
|
|
99
|
+
* distinction the branch below already makes; surfacing it lets a metrics
|
|
100
|
+
* consumer separate queue wait from cold-load time without another channel.
|
|
101
|
+
*
|
|
60
102
|
* Swaps drop the old session + model refs BEFORE loading the new
|
|
61
103
|
* checkpoint so GC + native destructors can reclaim the old weights
|
|
62
104
|
* during the load. A load failure leaves no resident (next call
|
|
63
105
|
* retries); a failure thrown by `fn` rejects only this call's promise
|
|
64
106
|
* and keeps the resident loaded for later callers.
|
|
65
107
|
*/
|
|
66
|
-
runWithResident<T>(modelId: string, fn: (session: ChatSession) => Promise<T>): Promise<T>;
|
|
108
|
+
runWithResident<T>(modelId: string, fn: (session: ChatSession, resident: boolean) => Promise<T>): Promise<T>;
|
|
67
109
|
/**
|
|
68
110
|
* Flag the current resident as post-error so the next turn does a full
|
|
69
111
|
* reset instead of a warm reuse. No-op unless `modelId` is the live
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model-host.d.ts","sourceRoot":"","sources":["../../src/provider/model-host.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,WAAW,EAAE,SAAS,EAA4B,MAAM,cAAc,CAAC;AAEhF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAEvD,MAAM,WAAW,mBAAmB;IAClC,gEAAgE;IAChE,WAAW,CAAC,EAAE,OAAO,SAAS,CAAC;IAC/B
|
|
1
|
+
{"version":3,"file":"model-host.d.ts","sourceRoot":"","sources":["../../src/provider/model-host.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,WAAW,EAAE,SAAS,EAA4B,MAAM,cAAc,CAAC;AAEhF,OAAO,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAC7D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAEvD;;;;;;;;GAQG;AACH,OAAO,EAAE,0BAA0B,EAAE,CAAC;AAEtC,gFAAgF;AAChF,MAAM,WAAW,eAAe;IAC9B;;;;;;;;;;OAUG;IACH,iBAAiB,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,WAAW,mBAAmB;IAClC,gEAAgE;IAChE,WAAW,CAAC,EAAE,OAAO,SAAS,CAAC;IAC/B;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,EAAE,MAAM,CAAC,EAAE,eAAe,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/F;;;;;;;OAOG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B;;;;;;OAMG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAmBD,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA0C;IACjE,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAmB;IAC/C,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAA4E;IAC/G,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAU;IAC5C,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAU;IAC5C,OAAO,CAAC,QAAQ,CAA8B;IAC9C,OAAO,CAAC,KAAK,CAAuC;IAEpD,YAAY,MAAM,EAAE,mBAAmB,EAAE,EAAE,IAAI,GAAE,mBAAwB,EAMxE;IAED,IAAI,UAAU,IAAI,MAAM,GAAG,IAAI,CAE9B;IAED;;;;;OAKG;IACH,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS,CAE1D;IAED;;;;;;;;;;;;;;;;;;;OAmBG;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,CAgD3G;IAED;;;;;OAKG;IACH,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAIvC;IAED;;;;;OAKG;IACH,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAO7C;IAED;;;;OAIG;IACH,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAIxC;IAED;;;;OAIG;IACH,OAAO,CAAC,aAAa;CAQtB"}
|
|
@@ -13,11 +13,23 @@
|
|
|
13
13
|
* overlapping native activity on the compiled-path globals).
|
|
14
14
|
*/
|
|
15
15
|
import { ChatSession, loadModel } from '@mlx-node/lm';
|
|
16
|
+
import { COLD_TIER_RESTORE_FAMILIES } from '../cold-tier.js';
|
|
17
|
+
/**
|
|
18
|
+
* Re-exported so the HOST consults exactly the symbol the drift guard and the
|
|
19
|
+
* `--no-persist-cache` help text consult. The definition lives in the
|
|
20
|
+
* native-free `../cold-tier.js` leaf (reachable off-package through the
|
|
21
|
+
* `@mlx-node/agent/catalog` subpath, which re-exports it)
|
|
22
|
+
* because this module value-imports `@mlx-node/lm`, which loads the native
|
|
23
|
+
* addon — the dashboard and the CLI help path must be able to read the list
|
|
24
|
+
* without that.
|
|
25
|
+
*/
|
|
26
|
+
export { COLD_TIER_RESTORE_FAMILIES };
|
|
16
27
|
export class MlxModelHost {
|
|
17
28
|
byName = new Map();
|
|
18
29
|
loadModelFn;
|
|
19
30
|
resolveModelPathFn;
|
|
20
31
|
requirePagedCache;
|
|
32
|
+
persistPagedCache;
|
|
21
33
|
resident = null;
|
|
22
34
|
chain = Promise.resolve();
|
|
23
35
|
constructor(models, opts = {}) {
|
|
@@ -26,6 +38,7 @@ export class MlxModelHost {
|
|
|
26
38
|
this.loadModelFn = opts.loadModelFn ?? loadModel;
|
|
27
39
|
this.resolveModelPathFn = opts.resolveModelPathFn ?? (async (model) => model.path);
|
|
28
40
|
this.requirePagedCache = opts.requirePagedCache ?? false;
|
|
41
|
+
this.persistPagedCache = opts.persistPagedCache ?? true;
|
|
29
42
|
}
|
|
30
43
|
get residentId() {
|
|
31
44
|
return this.resident?.id ?? null;
|
|
@@ -47,6 +60,12 @@ export class MlxModelHost {
|
|
|
47
60
|
* use the resident session; there is deliberately no method that
|
|
48
61
|
* returns a session outside the serialized section.
|
|
49
62
|
*
|
|
63
|
+
* `fn` also receives a `resident` boolean: `true` when the turn reused
|
|
64
|
+
* the already-loaded model (warm — no load happened), `false` when it had
|
|
65
|
+
* to load or swap the checkpoint first. This is the same warm/cold
|
|
66
|
+
* distinction the branch below already makes; surfacing it lets a metrics
|
|
67
|
+
* consumer separate queue wait from cold-load time without another channel.
|
|
68
|
+
*
|
|
50
69
|
* Swaps drop the old session + model refs BEFORE loading the new
|
|
51
70
|
* checkpoint so GC + native destructors can reclaim the old weights
|
|
52
71
|
* during the load. A load failure leaves no resident (next call
|
|
@@ -61,12 +80,25 @@ export class MlxModelHost {
|
|
|
61
80
|
}
|
|
62
81
|
return this.runSerialized(async () => {
|
|
63
82
|
let session;
|
|
83
|
+
// `true` when this turn reuses the loaded resident (warm), `false` when
|
|
84
|
+
// it loaded/swapped the checkpoint first (cold).
|
|
85
|
+
let resident;
|
|
64
86
|
if (this.resident?.id === modelId) {
|
|
65
87
|
session = this.resident.session;
|
|
88
|
+
resident = true;
|
|
66
89
|
}
|
|
67
90
|
else {
|
|
91
|
+
resident = false;
|
|
68
92
|
this.resident = null;
|
|
69
|
-
|
|
93
|
+
// Only a COLD_TIER_RESTORE_FAMILIES family has a sound paged cold
|
|
94
|
+
// restore. Hand it an EXPLICIT tri-state directive so the overlay can
|
|
95
|
+
// authoritatively set the flag either way (default-on, or
|
|
96
|
+
// `--no-persist-cache` off — overriding any value in the checkpoint's
|
|
97
|
+
// config.json). Every other family gets no policy at all, so the
|
|
98
|
+
// overlay never touches the field for them.
|
|
99
|
+
const resolvedPath = COLD_TIER_RESTORE_FAMILIES.has(entry.modelType)
|
|
100
|
+
? await this.resolveModelPathFn(entry, { persistPagedCache: this.persistPagedCache })
|
|
101
|
+
: await this.resolveModelPathFn(entry);
|
|
70
102
|
const model = await this.loadModelFn(resolvedPath);
|
|
71
103
|
const sessionModel = model;
|
|
72
104
|
const gemmaDraftActive = entry.modelType === 'gemma4' && sessionModel.hasMtpWeights?.() === true;
|
|
@@ -83,7 +115,7 @@ export class MlxModelHost {
|
|
|
83
115
|
session = new ChatSession(sessionModel);
|
|
84
116
|
this.resident = { id: modelId, session, model, dirty: false };
|
|
85
117
|
}
|
|
86
|
-
return await fn(session);
|
|
118
|
+
return await fn(session, resident);
|
|
87
119
|
});
|
|
88
120
|
}
|
|
89
121
|
/**
|
|
@@ -1,36 +1,92 @@
|
|
|
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
|
*/
|
|
15
|
-
interface
|
|
46
|
+
interface RuntimeModel {
|
|
16
47
|
provider: string;
|
|
17
48
|
id: string;
|
|
18
49
|
api: string;
|
|
19
50
|
baseUrl: string;
|
|
20
51
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
52
|
+
/** Minimal shape of a pi `Provider` — only the id is needed to gate on provider. */
|
|
53
|
+
interface RuntimeProvider {
|
|
54
|
+
id: string;
|
|
55
|
+
}
|
|
56
|
+
/** Minimal shape of a pi `CredentialInfo` — only the provider id is needed. */
|
|
57
|
+
interface RuntimeCredential {
|
|
58
|
+
providerId: string;
|
|
59
|
+
}
|
|
60
|
+
/** Minimal shape of a pi `AuthStatus` — only the configured flag is asserted. */
|
|
61
|
+
interface RuntimeAuthStatus {
|
|
62
|
+
configured: boolean;
|
|
63
|
+
}
|
|
64
|
+
export interface FilterableModelRuntime<TModel extends RuntimeModel = RuntimeModel> {
|
|
65
|
+
getModels(providerId?: string): readonly TModel[];
|
|
66
|
+
getAvailableSnapshot(): readonly TModel[];
|
|
67
|
+
getAvailable(providerId?: string): Promise<readonly TModel[]>;
|
|
68
|
+
getModel(provider: string, modelId: string): TModel | undefined;
|
|
69
|
+
getProvider(providerId: string): RuntimeProvider | undefined;
|
|
70
|
+
hasConfiguredAuth(providerId: string): boolean;
|
|
71
|
+
checkAuth(providerId: string): Promise<unknown>;
|
|
72
|
+
isUsingOAuth(providerId: string): boolean;
|
|
73
|
+
getProviders(): readonly RuntimeProvider[];
|
|
74
|
+
listCredentials(): Promise<readonly RuntimeCredential[]>;
|
|
75
|
+
getProviderAuthStatus(providerId: string): RuntimeAuthStatus;
|
|
76
|
+
login(providerId: string, type: unknown, interaction: unknown): Promise<unknown>;
|
|
77
|
+
refresh(options?: {
|
|
78
|
+
allowNetwork?: boolean;
|
|
79
|
+
force?: boolean;
|
|
80
|
+
signal?: unknown;
|
|
81
|
+
}): Promise<unknown>;
|
|
26
82
|
}
|
|
27
|
-
export interface
|
|
28
|
-
prototype:
|
|
83
|
+
export interface FilterableModelRuntimeConstructor<TModel extends RuntimeModel = RuntimeModel> {
|
|
84
|
+
prototype: FilterableModelRuntime<TModel>;
|
|
29
85
|
}
|
|
30
86
|
/**
|
|
31
|
-
* Install an exact local-model
|
|
32
|
-
* Returns an idempotent restore callback.
|
|
87
|
+
* Install an exact local-model / mlx-only-provider policy for one `runAgent()`
|
|
88
|
+
* lifetime. Returns an idempotent restore callback.
|
|
33
89
|
*/
|
|
34
|
-
export declare function installMlxOnlyModelRegistryFilter<TModel extends
|
|
90
|
+
export declare function installMlxOnlyModelRegistryFilter<TModel extends RuntimeModel>(Runtime: FilterableModelRuntimeConstructor<TModel>, modelIds: Iterable<string>): () => void;
|
|
35
91
|
export {};
|
|
36
92
|
//# sourceMappingURL=model-registry-filter.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model-registry-filter.d.ts","sourceRoot":"","sources":["../../src/provider/model-registry-filter.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"model-registry-filter.d.ts","sourceRoot":"","sources":["../../src/provider/model-registry-filter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAIH,UAAU,YAAY;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,oFAAoF;AACpF,UAAU,eAAe;IACvB,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,+EAA+E;AAC/E,UAAU,iBAAiB;IACzB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,iFAAiF;AACjF,UAAU,iBAAiB;IACzB,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,sBAAsB,CAAC,MAAM,SAAS,YAAY,GAAG,YAAY;IAChF,SAAS,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC;IAClD,oBAAoB,IAAI,SAAS,MAAM,EAAE,CAAC;IAC1C,YAAY,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAAC;IAC9D,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IAChE,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAAC;IAC7D,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/C,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAChD,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC;IAC1C,YAAY,IAAI,SAAS,eAAe,EAAE,CAAC;IAC3C,eAAe,IAAI,OAAO,CAAC,SAAS,iBAAiB,EAAE,CAAC,CAAC;IACzD,qBAAqB,CAAC,UAAU,EAAE,MAAM,GAAG,iBAAiB,CAAC;IAC7D,KAAK,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACjF,OAAO,CAAC,OAAO,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACpG;AAED,MAAM,WAAW,iCAAiC,CAAC,MAAM,SAAS,YAAY,GAAG,YAAY;IAC3F,SAAS,EAAE,sBAAsB,CAAC,MAAM,CAAC,CAAC;CAC3C;AAqCD;;;GAGG;AACH,wBAAgB,iCAAiC,CAAC,MAAM,SAAS,YAAY,EAC3E,OAAO,EAAE,iCAAiC,CAAC,MAAM,CAAC,EAClD,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,GACzB,MAAM,IAAI,CA2MZ"}
|