import type { Child, ServeKind } from "./localServe.js"; import type { RequestRules } from "./requestRules.js"; import { type MemorySnapshot } from "./availableMemory.js"; /** The model processes behind `agency local serve`. The pool owns one * record per served model. It is the only code that starts or stops a * model process, and the only code that writes a record. The front door * asks it for a model to send a request to, and hands the model back when * the request ends. */ /** What is fixed about a served model once it is planned. * * model the name requests use for it * upstreamModel the string its process was started with. A request's * `model` is rewritten to this, because mlx_lm.server * loads whatever model a request names * label how its process is named in a message, such as * "mlx_lm.server for org/a" * rules the request rules of its chat runtime, or null * lazy loaded on its first request, and stopped when another * lazy model needs the memory. A model that is not lazy * loads at startup and is never stopped to make room * needBytes the memory loading it is expected to take * stopsOnClose whether closing the connection to the process stops the * request with this body. See `ChatRuntimeSpec` */ export type ModelPlan = { model: string; upstreamModel: string; label: string; kind: ServeKind; rules: RequestRules | null; lazy: boolean; needBytes: number; stopsOnClose: (body: Record) => boolean; }; /** A model process and the port it listens on. */ export type Running = { child: Child; port: number; }; /** What a model's process is doing. Each state has the fields that state * has, so a record cannot say "ready" and hold no process. * * A loading model has no process for the moment between the load * starting and the spawn returning. `loaded` settles when the load ends, * for requests that arrive during it. */ export type ModelState = { state: "stopped"; } | { state: "loading"; running: Running | null; loaded: Promise; } | { state: "ready"; running: Running; } | { state: "failed"; error: string; }; export type ModelRecord = { plan: ModelPlan; current: ModelState; requestsInProgress: number; /** When a request last started or ended, or the load finished. Null for * a model that has never been loaded. */ lastUsedAt: number | null; }; /** One model as `status()` and `GET /v1/agency/status` report it. */ export type ModelStatus = { model: string; state: ModelState["state"]; error: string | null; requestsInProgress: number; lastUsedAt: number | null; }; /** A model held for one request. Its count of requests in progress is * already raised, and `release` lowers it. A second call to `release` * does nothing. */ export type Held = { plan: ModelPlan; port: number; release: () => void; }; /** Why the pool will not hand out a model. * * not-loaded the model was unloaded, and nothing will load it * again until `load` is called * load-failed the load a request was waiting on failed * not-enough-memory a lazy model does not fit, and no loaded lazy model * is idle to be stopped for it * stopping the server is shutting down */ export type RefusalReason = "not-loaded" | "load-failed" | "not-enough-memory" | "stopping"; export declare class PoolRefusal extends Error { reason: RefusalReason; constructor(reason: RefusalReason, message: string); } /** How long `stopAll` gives a process to exit after SIGTERM before it * sends SIGKILL, and then how long it waits for that. */ export declare const STOP_GRACE_MS = 5000; /** How long after a process exits the pool waits before calling it a * failure. A terminal Ctrl-C reaches the model processes before it * reaches this one, so an exit can arrive just before `stopAll` does. */ export declare const EXIT_GRACE_MS = 250; export type PoolDeps = { /** Picks a port and starts the model's process. Returns as soon as the * process exists, long before the model has loaded. */ spawn: (plan: ModelPlan) => Promise; /** Resolves when the process answers. Rejects if it answers wrongly, or * when `gone` resolves, which says a process exited. */ waitReady: (plan: ModelPlan, running: Running, gone: Promise) => Promise; now: () => number; availableMemory: () => Promise; /** Load a lazy model even when the estimate says it does not fit. */ allowOvercommit: boolean; log: (line: string) => void; /** Resolves after `ms`. Tests pass one that does not wait. */ wait: (ms: number) => Promise; }; export type ModelPool = { /** The names of the served models, in plan order. */ models: () => string[]; status: () => ModelStatus[]; plan: (model: string) => ModelPlan | undefined; /** Holds a loaded model for one request, waiting for a load in progress * and starting one for a lazy model that is not loaded. Rejects with a * `PoolRefusal` for any other model that is not loaded. */ acquire: (model: string) => Promise; /** Starts a model's process and resolves when the model is ready. Does * nothing for a model that is ready already. */ load: (model: string) => Promise; /** Stops a model's process and resolves when it has exited. A load in * progress is ended, and the requests waiting on it are refused. */ unload: (model: string) => Promise; /** Refuses every later load and request, sends SIGTERM to every process, * waits up to `STOP_GRACE_MS` for them to exit, sends SIGKILL to those * still running, and waits for those exits. */ stopAll: () => Promise; /** Resolves with a message when a process dies after it was ready. */ failure: Promise; }; /** The loaded lazy model that has been idle longest, or undefined when * every loaded model is busy or is not lazy. A model that is not lazy is * never stopped to make room. */ export declare function evictionCandidate(records: ModelRecord[]): ModelRecord | undefined; export declare function createModelPool(plans: ModelPlan[], deps: PoolDeps): ModelPool;