@mlx-node/agent 0.0.7
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 +26 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/catalog.js +44 -0
- package/dist/extensions/approval-detail.d.ts +9 -0
- package/dist/extensions/approval-detail.d.ts.map +1 -0
- package/dist/extensions/approval-detail.js +53 -0
- package/dist/extensions/permission-gate.d.ts +30 -0
- package/dist/extensions/permission-gate.d.ts.map +1 -0
- package/dist/extensions/permission-gate.js +309 -0
- package/dist/extensions/subagent.d.ts +82 -0
- package/dist/extensions/subagent.d.ts.map +1 -0
- package/dist/extensions/subagent.js +539 -0
- package/dist/extensions/terminal-title.d.ts +10 -0
- package/dist/extensions/terminal-title.d.ts.map +1 -0
- package/dist/extensions/terminal-title.js +45 -0
- package/dist/extensions/trace-notice.d.ts +11 -0
- package/dist/extensions/trace-notice.d.ts.map +1 -0
- package/dist/extensions/trace-notice.js +34 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/provider/chat-config.d.ts +34 -0
- package/dist/provider/chat-config.d.ts.map +1 -0
- package/dist/provider/chat-config.js +103 -0
- package/dist/provider/convert-messages.d.ts +59 -0
- package/dist/provider/convert-messages.d.ts.map +1 -0
- package/dist/provider/convert-messages.js +248 -0
- package/dist/provider/error-coercion.d.ts +19 -0
- package/dist/provider/error-coercion.d.ts.map +1 -0
- package/dist/provider/error-coercion.js +38 -0
- package/dist/provider/events.d.ts +67 -0
- package/dist/provider/events.d.ts.map +1 -0
- package/dist/provider/events.js +307 -0
- package/dist/provider/index.d.ts +28 -0
- package/dist/provider/index.d.ts.map +1 -0
- package/dist/provider/index.js +64 -0
- package/dist/provider/inference-trace.d.ts +58 -0
- package/dist/provider/inference-trace.d.ts.map +1 -0
- package/dist/provider/inference-trace.js +205 -0
- package/dist/provider/model-host.d.ts +94 -0
- package/dist/provider/model-host.d.ts.map +1 -0
- package/dist/provider/model-host.js +134 -0
- package/dist/provider/model-registry-filter.d.ts +36 -0
- package/dist/provider/model-registry-filter.d.ts.map +1 -0
- package/dist/provider/model-registry-filter.js +82 -0
- package/dist/provider/models.d.ts +35 -0
- package/dist/provider/models.d.ts.map +1 -0
- package/dist/provider/models.js +132 -0
- package/dist/provider/performance-status.d.ts +28 -0
- package/dist/provider/performance-status.d.ts.map +1 -0
- package/dist/provider/performance-status.js +91 -0
- package/dist/provider/reasoning-tag-buffer.d.ts +23 -0
- package/dist/provider/reasoning-tag-buffer.d.ts.map +1 -0
- package/dist/provider/reasoning-tag-buffer.js +60 -0
- package/dist/provider/stream-adapter.d.ts +61 -0
- package/dist/provider/stream-adapter.d.ts.map +1 -0
- package/dist/provider/stream-adapter.js +358 -0
- package/dist/provider/tool-call-buffer.d.ts +30 -0
- package/dist/provider/tool-call-buffer.d.ts.map +1 -0
- package/dist/provider/tool-call-buffer.js +75 -0
- package/dist/provider/warm-reuse.d.ts +73 -0
- package/dist/provider/warm-reuse.d.ts.map +1 -0
- package/dist/provider/warm-reuse.js +88 -0
- package/dist/run-agent.d.ts +62 -0
- package/dist/run-agent.d.ts.map +1 -0
- package/dist/run-agent.js +86 -0
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +42 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `MlxModelHost` — single-resident, lazily-loaded model + `ChatSession`
|
|
3
|
+
* owner for the provider bridge.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors the CLI launch-claude swap semantics (drop-then-load, one
|
|
6
|
+
* serialized operation chain) without the registry/alias machinery: the
|
|
7
|
+
* agent process serves exactly one model at a time, and every operation
|
|
8
|
+
* that touches the resident runs on one promise chain. Crucially the
|
|
9
|
+
* resident check/load AND the caller's full inference callback execute
|
|
10
|
+
* inside the SAME serialized closure ({@link MlxModelHost.runWithResident}),
|
|
11
|
+
* so a queued swap to another model can never replace the resident while
|
|
12
|
+
* an earlier caller is still mid-turn on it (stale session handle,
|
|
13
|
+
* overlapping native activity on the compiled-path globals).
|
|
14
|
+
*/
|
|
15
|
+
import { ChatSession, loadModel } from '@mlx-node/lm';
|
|
16
|
+
import type { DiscoveredModelLike } from '../types.js';
|
|
17
|
+
export interface MlxModelHostOptions {
|
|
18
|
+
/** Injectable model loader so tests can stub native loading. */
|
|
19
|
+
loadModelFn?: typeof loadModel;
|
|
20
|
+
/**
|
|
21
|
+
* Optional load-path policy. `mlx agent` uses this to point the loader at
|
|
22
|
+
* an ephemeral config overlay with block-paged attention enabled while
|
|
23
|
+
* leaving the checkpoint directory untouched.
|
|
24
|
+
*/
|
|
25
|
+
resolveModelPathFn?: (model: DiscoveredModelLike) => Promise<string>;
|
|
26
|
+
/**
|
|
27
|
+
* Reject a loaded model unless its native paged-cache adapter is active.
|
|
28
|
+
* The agent entrypoint enables this so a model/platform incompatibility
|
|
29
|
+
* fails clearly instead of silently falling back to flat KV cache. Gemma4
|
|
30
|
+
* with an attached external draft is the deliberate exception used only by
|
|
31
|
+
* the agent's explicit draft opt-in: its DSpark / assistant speculative
|
|
32
|
+
* executor is currently flat-cache-only.
|
|
33
|
+
*/
|
|
34
|
+
requirePagedCache?: boolean;
|
|
35
|
+
}
|
|
36
|
+
export declare class MlxModelHost {
|
|
37
|
+
private readonly byName;
|
|
38
|
+
private readonly loadModelFn;
|
|
39
|
+
private readonly resolveModelPathFn;
|
|
40
|
+
private readonly requirePagedCache;
|
|
41
|
+
private resident;
|
|
42
|
+
private chain;
|
|
43
|
+
constructor(models: DiscoveredModelLike[], opts?: MlxModelHostOptions);
|
|
44
|
+
get residentId(): string | null;
|
|
45
|
+
/**
|
|
46
|
+
* Read-only lookup of the discovery record behind `modelId` (name, path,
|
|
47
|
+
* `ModelType`). Pure map read — never touches the serialized chain or
|
|
48
|
+
* the resident. The stream adapter uses it to pick the launch preset
|
|
49
|
+
* for the model it is about to run.
|
|
50
|
+
*/
|
|
51
|
+
modelInfo(modelId: string): DiscoveredModelLike | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* Make `modelId` resident (loading or swapping on demand) and run `fn`
|
|
54
|
+
* against its `ChatSession` — both inside one serialized closure, so no
|
|
55
|
+
* other queued operation (in particular a swap to a different model)
|
|
56
|
+
* can touch the resident until `fn` settles. This is the ONLY way to
|
|
57
|
+
* use the resident session; there is deliberately no method that
|
|
58
|
+
* returns a session outside the serialized section.
|
|
59
|
+
*
|
|
60
|
+
* Swaps drop the old session + model refs BEFORE loading the new
|
|
61
|
+
* checkpoint so GC + native destructors can reclaim the old weights
|
|
62
|
+
* during the load. A load failure leaves no resident (next call
|
|
63
|
+
* retries); a failure thrown by `fn` rejects only this call's promise
|
|
64
|
+
* and keeps the resident loaded for later callers.
|
|
65
|
+
*/
|
|
66
|
+
runWithResident<T>(modelId: string, fn: (session: ChatSession) => Promise<T>): Promise<T>;
|
|
67
|
+
/**
|
|
68
|
+
* Flag the current resident as post-error so the next turn does a full
|
|
69
|
+
* reset instead of a warm reuse. No-op unless `modelId` is the live
|
|
70
|
+
* resident (a load failure or a swap already dropped/replaced it, and a
|
|
71
|
+
* reloaded model starts with a clean cache).
|
|
72
|
+
*/
|
|
73
|
+
markResidentDirty(modelId: string): void;
|
|
74
|
+
/**
|
|
75
|
+
* Read-and-clear the resident's post-error `dirty` flag. Returns `true`
|
|
76
|
+
* only when `modelId` is the live resident AND it was dirty — the signal
|
|
77
|
+
* for the caller to run a full `session.reset()` this turn instead of the
|
|
78
|
+
* warm-reuse wipe.
|
|
79
|
+
*/
|
|
80
|
+
consumeResidentDirty(modelId: string): boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Drop the current resident so the next `runWithResident` reloads it from
|
|
83
|
+
* scratch. Used when a post-error full reset itself fails and the session
|
|
84
|
+
* can no longer be trusted. No-op unless `modelId` is the live resident.
|
|
85
|
+
*/
|
|
86
|
+
invalidateResident(modelId: string): void;
|
|
87
|
+
/**
|
|
88
|
+
* Run `fn` after every previously queued operation completes. The
|
|
89
|
+
* chain advances regardless of `fn`'s outcome — a rejection reaches
|
|
90
|
+
* only this call's returned promise, never later queued operations.
|
|
91
|
+
*/
|
|
92
|
+
private runSerialized;
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=model-host.d.ts.map
|
|
@@ -0,0 +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;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrE;;;;;;;OAOG;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,CAAkD;IACrF,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,EAKxE;IAED,IAAI,UAAU,IAAI,MAAM,GAAG,IAAI,CAE9B;IAED;;;;;OAKG;IACH,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS,CAE1D;IAED;;;;;;;;;;;;;OAaG;IACH,eAAe,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAmCxF;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"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `MlxModelHost` — single-resident, lazily-loaded model + `ChatSession`
|
|
3
|
+
* owner for the provider bridge.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors the CLI launch-claude swap semantics (drop-then-load, one
|
|
6
|
+
* serialized operation chain) without the registry/alias machinery: the
|
|
7
|
+
* agent process serves exactly one model at a time, and every operation
|
|
8
|
+
* that touches the resident runs on one promise chain. Crucially the
|
|
9
|
+
* resident check/load AND the caller's full inference callback execute
|
|
10
|
+
* inside the SAME serialized closure ({@link MlxModelHost.runWithResident}),
|
|
11
|
+
* so a queued swap to another model can never replace the resident while
|
|
12
|
+
* an earlier caller is still mid-turn on it (stale session handle,
|
|
13
|
+
* overlapping native activity on the compiled-path globals).
|
|
14
|
+
*/
|
|
15
|
+
import { ChatSession, loadModel } from '@mlx-node/lm';
|
|
16
|
+
export class MlxModelHost {
|
|
17
|
+
byName = new Map();
|
|
18
|
+
loadModelFn;
|
|
19
|
+
resolveModelPathFn;
|
|
20
|
+
requirePagedCache;
|
|
21
|
+
resident = null;
|
|
22
|
+
chain = Promise.resolve();
|
|
23
|
+
constructor(models, opts = {}) {
|
|
24
|
+
for (const model of models)
|
|
25
|
+
this.byName.set(model.name, model);
|
|
26
|
+
this.loadModelFn = opts.loadModelFn ?? loadModel;
|
|
27
|
+
this.resolveModelPathFn = opts.resolveModelPathFn ?? (async (model) => model.path);
|
|
28
|
+
this.requirePagedCache = opts.requirePagedCache ?? false;
|
|
29
|
+
}
|
|
30
|
+
get residentId() {
|
|
31
|
+
return this.resident?.id ?? null;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Read-only lookup of the discovery record behind `modelId` (name, path,
|
|
35
|
+
* `ModelType`). Pure map read — never touches the serialized chain or
|
|
36
|
+
* the resident. The stream adapter uses it to pick the launch preset
|
|
37
|
+
* for the model it is about to run.
|
|
38
|
+
*/
|
|
39
|
+
modelInfo(modelId) {
|
|
40
|
+
return this.byName.get(modelId);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Make `modelId` resident (loading or swapping on demand) and run `fn`
|
|
44
|
+
* against its `ChatSession` — both inside one serialized closure, so no
|
|
45
|
+
* other queued operation (in particular a swap to a different model)
|
|
46
|
+
* can touch the resident until `fn` settles. This is the ONLY way to
|
|
47
|
+
* use the resident session; there is deliberately no method that
|
|
48
|
+
* returns a session outside the serialized section.
|
|
49
|
+
*
|
|
50
|
+
* Swaps drop the old session + model refs BEFORE loading the new
|
|
51
|
+
* checkpoint so GC + native destructors can reclaim the old weights
|
|
52
|
+
* during the load. A load failure leaves no resident (next call
|
|
53
|
+
* retries); a failure thrown by `fn` rejects only this call's promise
|
|
54
|
+
* and keeps the resident loaded for later callers.
|
|
55
|
+
*/
|
|
56
|
+
runWithResident(modelId, fn) {
|
|
57
|
+
const entry = this.byName.get(modelId);
|
|
58
|
+
if (!entry) {
|
|
59
|
+
const known = [...this.byName.keys()].join(', ');
|
|
60
|
+
return Promise.reject(new Error(`MlxModelHost: unknown model "${modelId}" (known models: ${known})`));
|
|
61
|
+
}
|
|
62
|
+
return this.runSerialized(async () => {
|
|
63
|
+
let session;
|
|
64
|
+
if (this.resident?.id === modelId) {
|
|
65
|
+
session = this.resident.session;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
this.resident = null;
|
|
69
|
+
const resolvedPath = await this.resolveModelPathFn(entry);
|
|
70
|
+
const model = await this.loadModelFn(resolvedPath);
|
|
71
|
+
const sessionModel = model;
|
|
72
|
+
const gemmaDraftActive = entry.modelType === 'gemma4' && sessionModel.hasMtpWeights?.() === true;
|
|
73
|
+
if (this.requirePagedCache && sessionModel.hasBlockPagedCache?.() !== true && !gemmaDraftActive) {
|
|
74
|
+
throw new Error(`MlxModelHost: model "${modelId}" (${entry.modelType}) loaded without an active ` +
|
|
75
|
+
`PagedAttention cache; this checkpoint, quantization, or platform is not compatible ` +
|
|
76
|
+
`with the mlx agent paged-cache requirement`);
|
|
77
|
+
}
|
|
78
|
+
if (this.requirePagedCache && entry.modelType === 'qwen3_5_moe' && sessionModel.hasMtpWeights?.() === true) {
|
|
79
|
+
throw new Error(`MlxModelHost: model "${modelId}" has Qwen3.5 MoE MTP weights, but the native MoE ` +
|
|
80
|
+
`backend cannot combine MTP with PagedAttention yet; refusing to silently downgrade ` +
|
|
81
|
+
`this agent session to paged autoregressive decoding`);
|
|
82
|
+
}
|
|
83
|
+
session = new ChatSession(sessionModel);
|
|
84
|
+
this.resident = { id: modelId, session, model, dirty: false };
|
|
85
|
+
}
|
|
86
|
+
return await fn(session);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Flag the current resident as post-error so the next turn does a full
|
|
91
|
+
* reset instead of a warm reuse. No-op unless `modelId` is the live
|
|
92
|
+
* resident (a load failure or a swap already dropped/replaced it, and a
|
|
93
|
+
* reloaded model starts with a clean cache).
|
|
94
|
+
*/
|
|
95
|
+
markResidentDirty(modelId) {
|
|
96
|
+
if (this.resident?.id === modelId) {
|
|
97
|
+
this.resident.dirty = true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Read-and-clear the resident's post-error `dirty` flag. Returns `true`
|
|
102
|
+
* only when `modelId` is the live resident AND it was dirty — the signal
|
|
103
|
+
* for the caller to run a full `session.reset()` this turn instead of the
|
|
104
|
+
* warm-reuse wipe.
|
|
105
|
+
*/
|
|
106
|
+
consumeResidentDirty(modelId) {
|
|
107
|
+
if (this.resident?.id !== modelId) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
const wasDirty = this.resident.dirty;
|
|
111
|
+
this.resident.dirty = false;
|
|
112
|
+
return wasDirty;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Drop the current resident so the next `runWithResident` reloads it from
|
|
116
|
+
* scratch. Used when a post-error full reset itself fails and the session
|
|
117
|
+
* can no longer be trusted. No-op unless `modelId` is the live resident.
|
|
118
|
+
*/
|
|
119
|
+
invalidateResident(modelId) {
|
|
120
|
+
if (this.resident?.id === modelId) {
|
|
121
|
+
this.resident = null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Run `fn` after every previously queued operation completes. The
|
|
126
|
+
* chain advances regardless of `fn`'s outcome — a rejection reaches
|
|
127
|
+
* only this call's returned promise, never later queued operations.
|
|
128
|
+
*/
|
|
129
|
+
runSerialized(fn) {
|
|
130
|
+
const result = this.chain.then(fn);
|
|
131
|
+
this.chain = result.then(() => undefined, () => undefined);
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-local policy adapter for pi's public ModelRegistry.
|
|
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.
|
|
10
|
+
*
|
|
11
|
+
* Keep this adapter isolated: once pi exposes a first-class provider allowlist
|
|
12
|
+
* in `MainOptions`, this file can be replaced by that option without touching
|
|
13
|
+
* the provider or CLI layers.
|
|
14
|
+
*/
|
|
15
|
+
interface RegistryModel {
|
|
16
|
+
provider: string;
|
|
17
|
+
id: string;
|
|
18
|
+
api: string;
|
|
19
|
+
baseUrl: string;
|
|
20
|
+
}
|
|
21
|
+
export interface FilterableModelRegistry<TModel extends RegistryModel = RegistryModel> {
|
|
22
|
+
getAll(): TModel[];
|
|
23
|
+
getAvailable(): TModel[];
|
|
24
|
+
find(provider: string, modelId: string): TModel | undefined;
|
|
25
|
+
hasConfiguredAuth(model: TModel): boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface FilterableModelRegistryConstructor<TModel extends RegistryModel = RegistryModel> {
|
|
28
|
+
prototype: FilterableModelRegistry<TModel>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Install an exact local-model read policy for one `runAgent()` lifetime.
|
|
32
|
+
* Returns an idempotent restore callback.
|
|
33
|
+
*/
|
|
34
|
+
export declare function installMlxOnlyModelRegistryFilter<TModel extends RegistryModel>(Registry: FilterableModelRegistryConstructor<TModel>, modelIds: Iterable<string>): () => void;
|
|
35
|
+
export {};
|
|
36
|
+
//# sourceMappingURL=model-registry-filter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model-registry-filter.d.ts","sourceRoot":"","sources":["../../src/provider/model-registry-filter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,UAAU,aAAa;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,uBAAuB,CAAC,MAAM,SAAS,aAAa,GAAG,aAAa;IACnF,MAAM,IAAI,MAAM,EAAE,CAAC;IACnB,YAAY,IAAI,MAAM,EAAE,CAAC;IACzB,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IAC5D,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;CAC3C;AAED,MAAM,WAAW,kCAAkC,CAAC,MAAM,SAAS,aAAa,GAAG,aAAa;IAC9F,SAAS,EAAE,uBAAuB,CAAC,MAAM,CAAC,CAAC;CAC5C;AAcD;;;GAGG;AACH,wBAAgB,iCAAiC,CAAC,MAAM,SAAS,aAAa,EAC5E,QAAQ,EAAE,kCAAkC,CAAC,MAAM,CAAC,EACpD,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,GACzB,MAAM,IAAI,CA0DZ"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-local policy adapter for pi's public ModelRegistry.
|
|
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.
|
|
10
|
+
*
|
|
11
|
+
* Keep this adapter isolated: once pi exposes a first-class provider allowlist
|
|
12
|
+
* in `MainOptions`, this file can be replaced by that option without touching
|
|
13
|
+
* the provider or CLI layers.
|
|
14
|
+
*/
|
|
15
|
+
const activePrototypes = new WeakSet();
|
|
16
|
+
function requireMethodDescriptor(prototype, name) {
|
|
17
|
+
const descriptor = Object.getOwnPropertyDescriptor(prototype, name);
|
|
18
|
+
if (!descriptor || typeof descriptor.value !== 'function' || descriptor.writable !== true) {
|
|
19
|
+
throw new Error(`mlx agent: incompatible pi ModelRegistry.${name}; expected a writable prototype method`);
|
|
20
|
+
}
|
|
21
|
+
return descriptor;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Install an exact local-model read policy for one `runAgent()` lifetime.
|
|
25
|
+
* Returns an idempotent restore callback.
|
|
26
|
+
*/
|
|
27
|
+
export function installMlxOnlyModelRegistryFilter(Registry, modelIds) {
|
|
28
|
+
const prototype = Registry.prototype;
|
|
29
|
+
if (activePrototypes.has(prototype)) {
|
|
30
|
+
throw new Error('mlx agent: concurrent ModelRegistry filtering in one process is not supported');
|
|
31
|
+
}
|
|
32
|
+
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;
|
|
44
|
+
Object.defineProperties(prototype, {
|
|
45
|
+
getAll: {
|
|
46
|
+
...descriptors.getAll,
|
|
47
|
+
value() {
|
|
48
|
+
return getAll.call(this).filter(isAllowed);
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
getAvailable: {
|
|
52
|
+
...descriptors.getAvailable,
|
|
53
|
+
value() {
|
|
54
|
+
return getAvailable.call(this).filter(isAllowed);
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
find: {
|
|
58
|
+
...descriptors.find,
|
|
59
|
+
value(provider, modelId) {
|
|
60
|
+
if (provider !== 'mlx' || !allowedIds.has(modelId))
|
|
61
|
+
return undefined;
|
|
62
|
+
const model = find.call(this, provider, modelId);
|
|
63
|
+
return model && isAllowed(model) ? model : undefined;
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
hasConfiguredAuth: {
|
|
67
|
+
...descriptors.hasConfiguredAuth,
|
|
68
|
+
value(model) {
|
|
69
|
+
return isAllowed(model) && hasConfiguredAuth.call(this, model);
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
activePrototypes.add(prototype);
|
|
74
|
+
let restored = false;
|
|
75
|
+
return () => {
|
|
76
|
+
if (restored)
|
|
77
|
+
return;
|
|
78
|
+
Object.defineProperties(prototype, descriptors);
|
|
79
|
+
activePrototypes.delete(prototype);
|
|
80
|
+
restored = true;
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local model discovery for the mlx pi provider.
|
|
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
|
|
7
|
+
* pairs every discovered checkpoint with a pi `ProviderModelConfig` entry
|
|
8
|
+
* ready for `pi.registerProvider('mlx', { models })`.
|
|
9
|
+
*
|
|
10
|
+
* `contextWindow` starts as the checkpoint's trained window, read from the model dir's
|
|
11
|
+
* `config.json` `max_position_embeddings` (root first, then the
|
|
12
|
+
* `text_config` nesting used by qwen3_5 / qwen3_5_moe / gemma4 unified
|
|
13
|
+
* checkpoints). Once a Qwen model loads, the provider narrows this shared
|
|
14
|
+
* model metadata to the physical paged-cache window so pi's later
|
|
15
|
+
* auto-compaction thresholds match reality. When both config fields are
|
|
16
|
+
* absent the documented per-family fallback below applies.
|
|
17
|
+
*/
|
|
18
|
+
import type { ProviderModelConfig } from '@earendil-works/pi-coding-agent';
|
|
19
|
+
import type { DiscoveredModelLike } from '../types.js';
|
|
20
|
+
/** A discovered local checkpoint paired with its pi provider model entry. */
|
|
21
|
+
export interface MlxModelInfo {
|
|
22
|
+
discovered: DiscoveredModelLike;
|
|
23
|
+
piModel: ProviderModelConfig;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Scan `modelsDir` for chat-capable model subdirectories and build their
|
|
27
|
+
* pi provider entries. Same tolerance as the cli discover walk: an
|
|
28
|
+
* unreadable dir yields `[]`; entries with an undetectable config, a
|
|
29
|
+
* non-generative type, or no launch preset are skipped silently
|
|
30
|
+
* (warnings only when `MLX_DEBUG` is set). Cheap by contract — no
|
|
31
|
+
* weights are loaded here. Results are sorted by directory name, which
|
|
32
|
+
* becomes both the pi model `id` and display `name`.
|
|
33
|
+
*/
|
|
34
|
+
export declare function discoverMlxModels(modelsDir: string): Promise<MlxModelInfo[]>;
|
|
35
|
+
//# sourceMappingURL=models.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local model discovery for the mlx pi provider.
|
|
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
|
|
7
|
+
* pairs every discovered checkpoint with a pi `ProviderModelConfig` entry
|
|
8
|
+
* ready for `pi.registerProvider('mlx', { models })`.
|
|
9
|
+
*
|
|
10
|
+
* `contextWindow` starts as the checkpoint's trained window, read from the model dir's
|
|
11
|
+
* `config.json` `max_position_embeddings` (root first, then the
|
|
12
|
+
* `text_config` nesting used by qwen3_5 / qwen3_5_moe / gemma4 unified
|
|
13
|
+
* checkpoints). Once a Qwen model loads, the provider narrows this shared
|
|
14
|
+
* model metadata to the physical paged-cache window so pi's later
|
|
15
|
+
* auto-compaction thresholds match reality. When both config fields are
|
|
16
|
+
* absent the documented per-family fallback below applies.
|
|
17
|
+
*/
|
|
18
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
19
|
+
import { basename, join } from 'node:path';
|
|
20
|
+
import { detectModelType } from '@mlx-node/lm';
|
|
21
|
+
import { launchPresetFor } from './chat-config.js';
|
|
22
|
+
// Non-generative detection results that cannot back a chat endpoint
|
|
23
|
+
// (mirrors the cli discover walk).
|
|
24
|
+
const NON_GENERATIVE = new Set(['harrier', 'qianfan-ocr', 'internvl_chat']);
|
|
25
|
+
/**
|
|
26
|
+
* Keyed by `ModelType`: a chat-capable family must have BOTH an entry
|
|
27
|
+
* here and a launch preset via `launchPresetFor` (which serves `lfm2_moe`
|
|
28
|
+
* from the agent-local MoE preset) to be served — missing either side is
|
|
29
|
+
* skipped, never guessed.
|
|
30
|
+
*/
|
|
31
|
+
const FAMILY_TRAITS = {
|
|
32
|
+
qwen3: { reasoning: true, fallbackContextWindow: 40960 },
|
|
33
|
+
qwen3_5: { reasoning: true, fallbackContextWindow: 262144 },
|
|
34
|
+
qwen3_5_moe: { reasoning: true, fallbackContextWindow: 262144 },
|
|
35
|
+
gemma4: { reasoning: false, fallbackContextWindow: 131072 },
|
|
36
|
+
lfm2: { reasoning: true, fallbackContextWindow: 128000 },
|
|
37
|
+
lfm2_moe: { reasoning: true, fallbackContextWindow: 128000 },
|
|
38
|
+
};
|
|
39
|
+
function positiveInteger(value) {
|
|
40
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? Math.floor(value) : undefined;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Read the trained context window from `<modelPath>/config.json`:
|
|
44
|
+
* root `max_position_embeddings` first (qwen3, lfm2), then
|
|
45
|
+
* `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.
|
|
49
|
+
*/
|
|
50
|
+
async function readContextWindow(modelPath, fallback) {
|
|
51
|
+
try {
|
|
52
|
+
const raw = await readFile(join(modelPath, 'config.json'), 'utf-8');
|
|
53
|
+
const config = JSON.parse(raw);
|
|
54
|
+
const root = positiveInteger(config.max_position_embeddings);
|
|
55
|
+
if (root !== undefined)
|
|
56
|
+
return root;
|
|
57
|
+
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
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// fall through to the family fallback
|
|
66
|
+
}
|
|
67
|
+
return fallback;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Scan `modelsDir` for chat-capable model subdirectories and build their
|
|
71
|
+
* pi provider entries. Same tolerance as the cli discover walk: an
|
|
72
|
+
* unreadable dir yields `[]`; entries with an undetectable config, a
|
|
73
|
+
* non-generative type, or no launch preset are skipped silently
|
|
74
|
+
* (warnings only when `MLX_DEBUG` is set). Cheap by contract — no
|
|
75
|
+
* weights are loaded here. Results are sorted by directory name, which
|
|
76
|
+
* becomes both the pi model `id` and display `name`.
|
|
77
|
+
*/
|
|
78
|
+
export async function discoverMlxModels(modelsDir) {
|
|
79
|
+
const debug = Boolean(process.env.MLX_DEBUG);
|
|
80
|
+
let entries;
|
|
81
|
+
try {
|
|
82
|
+
entries = await readdir(modelsDir, { withFileTypes: true });
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
const out = [];
|
|
88
|
+
for (const entry of entries) {
|
|
89
|
+
if (!entry.isDirectory())
|
|
90
|
+
continue;
|
|
91
|
+
const full = join(modelsDir, entry.name);
|
|
92
|
+
let modelType;
|
|
93
|
+
try {
|
|
94
|
+
modelType = await detectModelType(full);
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
if (debug)
|
|
98
|
+
console.warn(`[mlx] skip ${full}: ${err.message}`);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (NON_GENERATIVE.has(modelType))
|
|
102
|
+
continue;
|
|
103
|
+
const preset = launchPresetFor(modelType);
|
|
104
|
+
if (!preset) {
|
|
105
|
+
if (debug)
|
|
106
|
+
console.warn(`[mlx] skip ${full}: no launch preset for ${modelType}`);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const traits = FAMILY_TRAITS[modelType];
|
|
110
|
+
if (!traits) {
|
|
111
|
+
if (debug)
|
|
112
|
+
console.warn(`[mlx] skip ${full}: no FAMILY_TRAITS entry for ${modelType}`);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const name = basename(full);
|
|
116
|
+
const contextWindow = await readContextWindow(full, traits.fallbackContextWindow);
|
|
117
|
+
out.push({
|
|
118
|
+
discovered: { name, path: full, modelType },
|
|
119
|
+
piModel: {
|
|
120
|
+
id: name,
|
|
121
|
+
name,
|
|
122
|
+
reasoning: traits.reasoning,
|
|
123
|
+
input: ['text'],
|
|
124
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
125
|
+
contextWindow,
|
|
126
|
+
maxTokens: preset.maxOutputTokens,
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
out.sort((a, b) => (a.discovered.name < b.discovered.name ? -1 : a.discovered.name > b.discovered.name ? 1 : 0));
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transient per-message inference telemetry for the interactive agent footer.
|
|
3
|
+
*
|
|
4
|
+
* Pi's `Usage` object drives token accounting and compaction, so native
|
|
5
|
+
* throughput must not be smuggled into it. The exact AssistantMessage object
|
|
6
|
+
* is delivered to extension `message_end` handlers before persistence; a
|
|
7
|
+
* provider-scoped WeakMap therefore carries the metrics to the TUI without
|
|
8
|
+
* changing the conversation schema or retaining completed messages.
|
|
9
|
+
*/
|
|
10
|
+
import type { AssistantMessage } from '@earendil-works/pi-ai';
|
|
11
|
+
import type { ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
12
|
+
import type { PerformanceMetrics } from '@mlx-node/lm';
|
|
13
|
+
interface MessageEndLike {
|
|
14
|
+
message: {
|
|
15
|
+
role: string;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export declare class PerformanceStatus {
|
|
19
|
+
private readonly byMessage;
|
|
20
|
+
/** Record only complete, displayable samples; malformed native metrics are ignored. */
|
|
21
|
+
readonly record: (message: AssistantMessage, performance: PerformanceMetrics) => void;
|
|
22
|
+
/** Render the successful mlx inference associated with this exact Pi message. */
|
|
23
|
+
showMessage(event: MessageEndLike, ctx: ExtensionContext): void;
|
|
24
|
+
/** Prevent the selected model's completed sample lingering after its lifecycle ends. */
|
|
25
|
+
clear(ctx: ExtensionContext): void;
|
|
26
|
+
}
|
|
27
|
+
export {};
|
|
28
|
+
//# sourceMappingURL=performance-status.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"performance-status.d.ts","sourceRoot":"","sources":["../../src/provider/performance-status.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAYvD,UAAU,cAAc;IACtB,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CAC3B;AAqDD,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAqD;IAE/E,uFAAuF;IACvF,QAAQ,CAAC,MAAM,YAAa,gBAAgB,eAAe,kBAAkB,KAAG,IAAI,CAclF;IAEF,iFAAiF;IACjF,WAAW,CAAC,KAAK,EAAE,cAAc,EAAE,GAAG,EAAE,gBAAgB,GAAG,IAAI,CAK9D;IAED,wFAAwF;IACxF,KAAK,CAAC,GAAG,EAAE,gBAAgB,GAAG,IAAI,CAEjC;CACF"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transient per-message inference telemetry for the interactive agent footer.
|
|
3
|
+
*
|
|
4
|
+
* Pi's `Usage` object drives token accounting and compaction, so native
|
|
5
|
+
* throughput must not be smuggled into it. The exact AssistantMessage object
|
|
6
|
+
* is delivered to extension `message_end` handlers before persistence; a
|
|
7
|
+
* provider-scoped WeakMap therefore carries the metrics to the TUI without
|
|
8
|
+
* changing the conversation schema or retaining completed messages.
|
|
9
|
+
*/
|
|
10
|
+
const STATUS_KEY = 'mlx-performance';
|
|
11
|
+
function finiteRate(value) {
|
|
12
|
+
return Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
13
|
+
}
|
|
14
|
+
function finiteTokenCount(value) {
|
|
15
|
+
return value !== undefined && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
|
|
16
|
+
}
|
|
17
|
+
function formatRate(value) {
|
|
18
|
+
return value.toLocaleString('en-US', {
|
|
19
|
+
minimumFractionDigits: 1,
|
|
20
|
+
maximumFractionDigits: 1,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
function formatDuration(ttftMs) {
|
|
24
|
+
if (ttftMs < 1000)
|
|
25
|
+
return `${formatRate(ttftMs)} ms`;
|
|
26
|
+
return `${(ttftMs / 1000).toLocaleString('en-US', {
|
|
27
|
+
minimumFractionDigits: 2,
|
|
28
|
+
maximumFractionDigits: 2,
|
|
29
|
+
})} s`;
|
|
30
|
+
}
|
|
31
|
+
function formatTokenCount(value) {
|
|
32
|
+
if (value < 1000)
|
|
33
|
+
return value.toLocaleString('en-US');
|
|
34
|
+
const [divisor, suffix] = value < 1_000_000 ? [1000, 'k'] : [1_000_000, 'm'];
|
|
35
|
+
return `${(value / divisor).toLocaleString('en-US', { maximumFractionDigits: 1 })}${suffix}`;
|
|
36
|
+
}
|
|
37
|
+
function formatContext(sample) {
|
|
38
|
+
let context = '';
|
|
39
|
+
if (sample.ttftMs !== undefined)
|
|
40
|
+
context += ` · TTFT ${formatDuration(sample.ttftMs)}`;
|
|
41
|
+
if (sample.inputTokens !== undefined && sample.cachedTokens !== undefined) {
|
|
42
|
+
if (sample.cachedTokens > 0) {
|
|
43
|
+
context +=
|
|
44
|
+
` · input ${formatTokenCount(sample.inputTokens)} new` + ` + ${formatTokenCount(sample.cachedTokens)} cached`;
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
context += ` · input ${formatTokenCount(sample.inputTokens)}`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return context;
|
|
51
|
+
}
|
|
52
|
+
function formatSample(sample) {
|
|
53
|
+
return (`mlx${formatContext(sample)}` +
|
|
54
|
+
` · prefill ${formatRate(sample.prefillTokensPerSecond)} tok/s` +
|
|
55
|
+
` · decode ${formatRate(sample.decodeTokensPerSecond)} tok/s`);
|
|
56
|
+
}
|
|
57
|
+
export class PerformanceStatus {
|
|
58
|
+
byMessage = new WeakMap();
|
|
59
|
+
/** Record only complete, displayable samples; malformed native metrics are ignored. */
|
|
60
|
+
record = (message, performance) => {
|
|
61
|
+
const prefillTokensPerSecond = finiteRate(performance.prefillTokensPerSecond);
|
|
62
|
+
const decodeTokensPerSecond = finiteRate(performance.decodeTokensPerSecond);
|
|
63
|
+
if (prefillTokensPerSecond === undefined || decodeTokensPerSecond === undefined)
|
|
64
|
+
return;
|
|
65
|
+
const sample = { prefillTokensPerSecond, decodeTokensPerSecond };
|
|
66
|
+
const ttftMs = finiteRate(performance.ttftMs);
|
|
67
|
+
if (ttftMs !== undefined)
|
|
68
|
+
sample.ttftMs = ttftMs;
|
|
69
|
+
const inputTokens = finiteTokenCount(message.usage?.input);
|
|
70
|
+
const cachedTokens = finiteTokenCount(message.usage?.cacheRead);
|
|
71
|
+
if (inputTokens !== undefined && cachedTokens !== undefined) {
|
|
72
|
+
sample.inputTokens = inputTokens;
|
|
73
|
+
sample.cachedTokens = cachedTokens;
|
|
74
|
+
}
|
|
75
|
+
this.byMessage.set(message, sample);
|
|
76
|
+
};
|
|
77
|
+
/** Render the successful mlx inference associated with this exact Pi message. */
|
|
78
|
+
showMessage(event, ctx) {
|
|
79
|
+
if (ctx.mode !== 'tui' || event.message.role !== 'assistant')
|
|
80
|
+
return;
|
|
81
|
+
const sample = this.byMessage.get(event.message);
|
|
82
|
+
if (!sample)
|
|
83
|
+
return;
|
|
84
|
+
ctx.ui.setStatus(STATUS_KEY, ctx.ui.theme.fg('dim', formatSample(sample)));
|
|
85
|
+
}
|
|
86
|
+
/** Prevent the selected model's completed sample lingering after its lifecycle ends. */
|
|
87
|
+
clear(ctx) {
|
|
88
|
+
if (ctx.mode === 'tui')
|
|
89
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
90
|
+
}
|
|
91
|
+
}
|