pi-llama-cpp 0.2.1 → 0.2.2

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/README.md CHANGED
@@ -18,9 +18,13 @@ A [Pi Coding Agent](https://pi.dev/) extension that integrates with a running [l
18
18
  | 🟢 | Loaded | Model is active and ready to use |
19
19
  | 🟡 | Loading | Model is currently being loaded |
20
20
  | 🔴 | Failed | Model failed to load |
21
- | 🔵 | Sleeping | Model is loaded but inactive (router mode) |
21
+ | 🔵 | Sleeping | Model is available, but inactive |
22
22
  | ⚪ | Unloaded | Model is not loaded on the server |
23
23
 
24
+ > **Note**: The `Sleeping` status only shows when you start your server with `llama-server --sleep-idle-seconds <n> ...`.
25
+ This is a **llama.cpp server flag** that tells the server to put idle models to sleep after `n` seconds.
26
+ The model awakens automatically when you send a message.
27
+
24
28
  ## Installation
25
29
 
26
30
  This package is a Pi extension. Install it with
@@ -65,6 +69,8 @@ If your llama.cpp server requires authentication, use `/login` in Pi, select the
65
69
 
66
70
  Alternatively, configure the API key in `~/.pi/agent/auth.json` using the provider ID `llama-server`:
67
71
 
72
+ > **Note**: The provider is displayed as **Llama.cpp** in the Pi UI, but its internal identifier is `llama-server` — use this ID when configuring `auth.json` or other programmatic access.
73
+
68
74
  ```json
69
75
  {
70
76
  "llama-server": {
@@ -86,21 +92,24 @@ Make sure your llama.cpp server is running with the appropriate flags.
86
92
  llama-server --models-preset path/to/presets.ini ...
87
93
  ```
88
94
 
89
- The extension reads the context size from the preset file using the `ctx-size` and/or `fit-ctx` keys.
90
-
91
95
  - For single-model mode, start the server with:
92
96
 
93
97
  ```bash
94
- llama-server --model path/to/model.gguf --ctx-size 128000 ...
98
+ llama-server --model path/to/model.gguf ...
95
99
  ```
96
100
 
101
+ The extension determines the context size as follows:
102
+ - **Router mode** — reads from the preset file's `ctx-size` and/or `fit-ctx` keys
103
+ - **Single mode** — reads from the `/slots` endpoint (stores it in cache afterwards)
104
+ - Falls back to `128000` if not available
105
+
97
106
  ### Commands
98
107
 
99
108
  | Command | Description |
100
109
  | --------- | ------------------------------------------------------------------------------------------ |
101
110
  | `/models` | Browse your models with live status. Select a model to load, switch, or unload it. |
102
111
 
103
- > **Note:** When the llama.cpp server is unreachable, `/models` is still available but displays an error notification with the configured server URL.
112
+ > **Note:** When the llama.cpp server is unreachable, `/models` is still available but shows the description `Llama.cpp models (offline)` and displays an error notification with the configured server URL.
104
113
 
105
114
  ### Model Actions
106
115
 
@@ -117,13 +126,20 @@ When browsing models via the `/models` command, you can:
117
126
 
118
127
  ### Model Selection Event
119
128
 
120
- When Pi switches models (via `model_select`), the extension automatically loads the selected model on the llama.cpp server. This keeps the server in sync with the active model in Pi.
129
+ When you switch models via Pi's model picker (instead of using the `/models` command), the extension listens for the `model_select` event, which also loads the requested model before the conversation begins.
130
+
131
+ This keeps the server in sync with the active model in Pi, regardless of how the switch was initiated — you don't need to manually load models before using them.
132
+
133
+ ### Loading Models
134
+
135
+ When you trigger a load, switch, or retry action, the extension polls the server to track progress. If a model takes longer than **60 seconds** to load, the polling times out with an error.
136
+ > **Note:** The timeout is only for the polling. The model might still be loading.
121
137
 
122
138
  ### Model Configuration
123
139
 
124
140
  Each model exposed to Pi includes the following defaults:
125
141
 
126
- - **`maxTokens`** — `16384` (maximum tokens per response)
142
+ - **`maxTokens`** — `32000` (maximum possible tokens per response according to Pi's source code)
127
143
  - **`reasoning`** — `true` (assumed, as llama.cpp's `/models` endpoint does not expose it)
128
144
  - **`cost`** — all zero (local model)
129
145
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-llama-cpp",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Pi extension for llama.cpp integration. Supports both router and single modes",
5
5
  "keywords": [
6
6
  "pi",
package/src/constants.ts CHANGED
@@ -21,7 +21,7 @@ export const DEFAULT_CTX = 128000;
21
21
  /**
22
22
  * Maximum number of tokens a model can generate in a single response
23
23
  */
24
- export const MAX_TOKENS = 16384;
24
+ export const MAX_TOKENS = 32000;
25
25
 
26
26
  /**
27
27
  * Polling interval (ms) for checking model load status
package/src/events.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
- import { PROVIDER_NAME } from "./constants";
3
- import { ModelSelectEvent } from "./interfaces/IModelSelectEvent";
2
+ import { PROVIDER_ID } from "./constants";
3
+ import { ModelSelectEvent } from "./interfaces/events";
4
4
  import { listModels } from "./tools/retriever";
5
5
 
6
6
  /**
@@ -12,7 +12,7 @@ export const onModelSelect = async (
12
12
  event: ModelSelectEvent,
13
13
  ctx: ExtensionContext,
14
14
  ) => {
15
- if (event.model.provider !== PROVIDER_NAME) return;
15
+ if (event.model.provider !== PROVIDER_ID) return;
16
16
 
17
17
  const models = await listModels();
18
18
  const model = models.find((m) => m.id === event.model.id);
package/src/handlers.ts CHANGED
@@ -35,7 +35,7 @@ const selectModel = async (
35
35
  const getActionsForModel = async (model: BaseModel): Promise<Array<Action>> => {
36
36
  const routerModeActions: Record<Status, Array<Action>> = {
37
37
  [Status.LOADED]: [Action.SWITCH, Action.UNLOAD, Action.INFO, Action.CANCEL],
38
- [Status.LOADING]: [Action.CANCEL],
38
+ [Status.LOADING]: [Action.INFO, Action.CANCEL],
39
39
  [Status.FAILED]: [Action.RETRY, Action.CANCEL],
40
40
  [Status.SLEEPING]: [Action.UNLOAD, Action.INFO, Action.CANCEL],
41
41
  [Status.UNLOADED]: [Action.LOAD, Action.CANCEL],
@@ -45,7 +45,7 @@ const getActionsForModel = async (model: BaseModel): Promise<Array<Action>> => {
45
45
  [Status.LOADED]: [Action.INFO, Action.CANCEL],
46
46
  [Status.LOADING]: [Action.CANCEL],
47
47
  [Status.FAILED]: [Action.CANCEL],
48
- [Status.SLEEPING]: [Action.CANCEL],
48
+ [Status.SLEEPING]: [Action.INFO, Action.CANCEL],
49
49
  [Status.UNLOADED]: [Action.CANCEL],
50
50
  };
51
51
 
@@ -0,0 +1,10 @@
1
+ import { PROVIDER_ID } from "../constants";
2
+
3
+ export interface Auth {
4
+ type: string;
5
+ key: string;
6
+ }
7
+
8
+ export interface AuthFile {
9
+ [PROVIDER_ID]: Auth;
10
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The structure of llama-server's /health endpoint
3
+ */
4
+ export interface HealthEndpoint {
5
+ status: "ok";
6
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * The structure of llama-server's /models endpoint
3
+ *
4
+ * In single mode, the `models` property is not returned
5
+ * In router mode, everything is used
6
+ */
7
+ export interface ModelsEndpoint {
8
+ models?: ModelProperty[];
9
+ object: string;
10
+ data: DataProperty[];
11
+ }
12
+
13
+ export interface ModelProperty {
14
+ name: string;
15
+ model: string;
16
+ modified_at: string;
17
+ size: string;
18
+ digest: string;
19
+ type: string;
20
+ description: string;
21
+ tags: string[];
22
+ capabilities: string[];
23
+ parameters: string;
24
+ details: {
25
+ parent_model: string;
26
+ format: string;
27
+ family: string;
28
+ families: string[];
29
+ parameter_size: string;
30
+ quantization_level: string;
31
+ };
32
+ }
33
+
34
+ export interface DataProperty {
35
+ id: string;
36
+ aliases?: string[];
37
+ tags: string[];
38
+ object: string;
39
+ owned_by: string;
40
+ created: number;
41
+ status?: StatusProperty;
42
+ meta?: MetaProperty;
43
+ }
44
+
45
+ interface StatusProperty {
46
+ value: string;
47
+ args: string[];
48
+ preset: string;
49
+ exit_code?: number;
50
+ failed?: boolean;
51
+ }
52
+
53
+ interface MetaProperty {
54
+ vocab_type: number;
55
+ n_vocab: number;
56
+ n_ctx_train: number;
57
+ n_embd: number;
58
+ n_params: number;
59
+ size: number;
60
+ }
@@ -0,0 +1,29 @@
1
+
2
+ /**
3
+ * The structure of llama-server's /props endpoint
4
+ *
5
+ * In single mode, applies to /props
6
+ * In router mode, applies to /props?model=<id>
7
+ */
8
+ export interface PropsEndpoint {
9
+ default_generation_settings: Record<string, any>;
10
+ total_slots: number;
11
+ model_alias: string;
12
+ model_path: string;
13
+ modalities: {
14
+ vision: boolean;
15
+ audio: boolean;
16
+ };
17
+ media_marker: string;
18
+ endpoint_slots: boolean;
19
+ endpoint_props: boolean;
20
+ endpoint_metrics: boolean;
21
+ webui: boolean;
22
+ webui_settings: Record<string, any>;
23
+ chat_template: string;
24
+ chat_template_caps: Record<string, boolean>;
25
+ bos_token: string;
26
+ eos_token: string;
27
+ build_info: string;
28
+ is_sleeping: boolean;
29
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The structure of llama-server's /slots endpoint
3
+ *
4
+ * In single mode, applies to /slots
5
+ * In router mode, applies to /slots?model=<id>
6
+ */
7
+ export interface SlotsEndpoint {
8
+ id: number;
9
+ n_ctx: number;
10
+ speculative: boolean;
11
+ is_processing: boolean;
12
+ id_task?: number;
13
+ params?: Array<Record<string, any>>;
14
+ next_token?: Array<Record<string, any>>;
15
+ }
@@ -2,9 +2,12 @@ import type { ProviderModelConfig } from "@mariozechner/pi-coding-agent";
2
2
  import { MAX_TOKENS, POLLING_INTERVAL, POLLING_TIMEOUT } from "../constants";
3
3
  import { Mode } from "../enums/mode";
4
4
  import { Status } from "../enums/status";
5
+ import { DataProperty } from "../interfaces/endpoints/models";
5
6
  import { rpc } from "../tools/retriever";
6
7
 
7
8
  export abstract class BaseModel {
9
+ constructor(protected readonly model: DataProperty) {}
10
+
8
11
  protected readonly statusMapper: Record<string, Status> = {
9
12
  loaded: Status.LOADED,
10
13
  loading: Status.LOADING,
@@ -23,9 +26,13 @@ export abstract class BaseModel {
23
26
 
24
27
  abstract get mode(): Mode;
25
28
 
26
- abstract get id(): string;
29
+ get id(): string {
30
+ return this.model.id;
31
+ }
27
32
 
28
- abstract get name(): string;
33
+ get name(): string {
34
+ return this.model.aliases?.[0] || this.model.id;
35
+ }
29
36
 
30
37
  get reasoning(): boolean {
31
38
  // We don't have a way to detect this, so we'll fallback to true
@@ -67,6 +74,7 @@ export abstract class BaseModel {
67
74
  `Reasoning : ${this.reasoning}`,
68
75
  `Capabilities : ${this.capabilities.join(", ")}`,
69
76
  `Context size : ${await this.getContextSize()}`,
77
+ `Status : ${await this.getStatus()}`,
70
78
  ];
71
79
 
72
80
  const response = `${messages.join("\n")}\n`;
@@ -1,40 +1,32 @@
1
1
  import { DEFAULT_CTX } from "../constants";
2
2
  import { Mode } from "../enums/mode";
3
3
  import { Status } from "../enums/status";
4
- import { IRouterModel } from "../interfaces/IRouterModel";
4
+ import { DataProperty, ModelsEndpoint } from "../interfaces/endpoints/models";
5
5
  import { rpc } from "../tools/retriever";
6
6
  import { BaseModel } from "./baseModel";
7
7
 
8
8
  export class RouterModel extends BaseModel {
9
- constructor(private readonly model: IRouterModel) {
10
- super();
9
+ constructor(protected readonly model: DataProperty) {
10
+ super(model);
11
11
  }
12
12
 
13
13
  get mode(): Mode {
14
14
  return Mode.ROUTER;
15
15
  }
16
16
 
17
- get id(): string {
18
- return this.model.id;
19
- }
20
-
21
- get name(): string {
22
- return this.model.aliases?.[0] || this.model.id;
23
- }
24
-
25
17
  get capabilities(): ["text"] | ["image"] {
26
- const hasImage = this.model.status.args?.includes("--mmproj") ?? false;
18
+ const hasImage = this.model.status!.args?.includes("--mmproj") ?? false;
27
19
  return hasImage ? ["image"] : ["text"];
28
20
  }
29
21
 
30
22
  async getStatus(): Promise<Status> {
31
- const { data } = await rpc<{ data: IRouterModel[] }>("/models");
23
+ const { data } = await rpc<ModelsEndpoint>("/models");
32
24
  const model = data.find((m) => m.id === this.id);
33
25
  if (!model) return Status.FAILED;
34
26
 
35
- const status = this.statusMapper[model.status.value];
27
+ const status = this.statusMapper[model.status!.value];
36
28
  if (status === Status.UNLOADED) {
37
- if (this.model.status.failed) return Status.FAILED;
29
+ if (this.model.status!.failed) return Status.FAILED;
38
30
 
39
31
  return Status.UNLOADED;
40
32
  }
@@ -58,7 +50,7 @@ export class RouterModel extends BaseModel {
58
50
  * @returns The value
59
51
  */
60
52
  private extractFrom(arg: string): number | null {
61
- const args = this.model.status.args;
53
+ const args = this.model.status!.args;
62
54
  if (!args) return null;
63
55
 
64
56
  const ctxIdx = args.indexOf(arg);
@@ -1,41 +1,50 @@
1
1
  import { DEFAULT_CTX } from "../constants";
2
2
  import { Mode } from "../enums/mode";
3
3
  import { Status } from "../enums/status";
4
- import { ISingleModel } from "../interfaces/ISingleModel";
4
+ import { DataProperty, ModelProperty } from "../interfaces/endpoints/models";
5
+ import { PropsEndpoint } from "../interfaces/endpoints/props";
6
+ import { SlotsEndpoint } from "../interfaces/endpoints/slots";
5
7
  import { rpc } from "../tools/retriever";
6
8
  import { BaseModel } from "./baseModel";
7
9
 
8
10
  export class SingleModel extends BaseModel {
9
- constructor(private readonly model: ISingleModel) {
10
- super();
11
+ private contextSize?: number;
12
+
13
+ constructor(
14
+ protected readonly model: DataProperty,
15
+ private readonly extra: ModelProperty,
16
+ ) {
17
+ super(model);
11
18
  }
12
19
 
13
20
  get mode(): Mode {
14
21
  return Mode.SINGLE;
15
22
  }
16
23
 
17
- get id(): string {
18
- return this.model.name;
19
- }
20
-
21
- get name(): string {
22
- return this.model.name;
23
- }
24
-
25
24
  get capabilities(): ["text"] | ["image"] {
26
- const hasImage = this.model.capabilities.includes("multimodal");
25
+ const hasImage = this.extra.capabilities.includes("multimodal");
27
26
  return hasImage ? ["image"] : ["text"];
28
27
  }
29
28
 
30
29
  async getStatus(): Promise<Status> {
31
30
  // In single-mode, the extension will only work when the model is fully loaded
31
+ const { is_sleeping } = await rpc<PropsEndpoint>("/props");
32
+ if (is_sleeping) return Status.SLEEPING;
33
+
32
34
  return Status.LOADED;
33
35
  }
34
36
 
35
37
  async getContextSize(): Promise<number> {
36
- const slots = await rpc<{ n_ctx: number }[]>("/slots");
37
- const [{ n_ctx }] = slots;
38
+ // Avoid calling the endpoint if we already have the value
39
+ if (this.contextSize) return this.contextSize;
40
+
41
+ try {
42
+ const [{ n_ctx }] = await rpc<SlotsEndpoint[]>("/slots");
43
+ this.contextSize = n_ctx;
38
44
 
39
- return n_ctx ?? DEFAULT_CTX;
45
+ return this.contextSize;
46
+ } catch {
47
+ return DEFAULT_CTX;
48
+ }
40
49
  }
41
50
  }
@@ -1,7 +1,7 @@
1
1
  import { access, constants, readFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { DEFAULT_LLAMA_SERVER_URL, PROVIDER_ID } from "../constants";
4
- import { IAuth, IAuthFile } from "../interfaces/IAuthFile";
4
+ import { Auth, AuthFile } from "../interfaces/auth";
5
5
 
6
6
  // The URL is detected once, to reuse forever
7
7
  let resolvedUrl: string | undefined;
@@ -60,7 +60,7 @@ export const resolveApiKey = async (): Promise<string> => {
60
60
  const authPath = join(process.env.HOME || ".", ".pi", "agent", "auth.json");
61
61
  if (!(await fileExists(authPath))) return placeholder;
62
62
 
63
- const cfg = await readConfigValue<IAuthFile, IAuth | null>(
63
+ const cfg = await readConfigValue<AuthFile, Auth | null>(
64
64
  authPath,
65
65
  PROVIDER_ID,
66
66
  );
@@ -1,5 +1,5 @@
1
- import { IRouterModel } from "../interfaces/IRouterModel";
2
- import { ISingleModel } from "../interfaces/ISingleModel";
1
+ import { HealthEndpoint } from "../interfaces/endpoints/health";
2
+ import { ModelsEndpoint } from "../interfaces/endpoints/models";
3
3
  import { BaseModel } from "../models/baseModel";
4
4
  import { RouterModel } from "../models/routerModel";
5
5
  import { SingleModel } from "../models/singleModel";
@@ -11,7 +11,7 @@ import { resolveApiKey, resolveUrl } from "./resolver";
11
11
  */
12
12
  export const isServerReady = async (): Promise<boolean> => {
13
13
  try {
14
- const { status } = await rpc<{ status: string }>("/health");
14
+ const { status } = await rpc<HealthEndpoint>("/health");
15
15
  return status === "ok";
16
16
  } catch {
17
17
  return false;
@@ -59,13 +59,11 @@ export const rpc = async <T>(
59
59
  * @returns The list of models
60
60
  */
61
61
  export const listModels = async (): Promise<BaseModel[]> => {
62
- const { models, data } = await rpc<{
63
- models?: ISingleModel[];
64
- data: IRouterModel[];
65
- }>("/models");
62
+ const { models, data } = await rpc<ModelsEndpoint>("/models");
66
63
 
67
64
  if (models) {
68
- return models.map((m) => new SingleModel(m));
65
+ const [extra] = models;
66
+ return data.map((m) => new SingleModel(m, extra));
69
67
  }
70
68
 
71
69
  const response = data
@@ -1,10 +0,0 @@
1
- import { PROVIDER_NAME } from "../constants";
2
-
3
- export interface IAuth {
4
- type: string;
5
- key: string;
6
- }
7
-
8
- export interface IAuthFile {
9
- [PROVIDER_NAME]: IAuth;
10
- }
@@ -1,17 +0,0 @@
1
- interface IRouterModelStatus {
2
- value: string;
3
- args: string[];
4
- preset: string;
5
- exit_code?: number;
6
- failed?: boolean;
7
- }
8
-
9
- export interface IRouterModel {
10
- id: string;
11
- aliases?: string[];
12
- tags: string[];
13
- object: string;
14
- owned_by: string;
15
- created: number;
16
- status: IRouterModelStatus;
17
- }
@@ -1,20 +0,0 @@
1
- export interface ISingleModel {
2
- name: string;
3
- model: string;
4
- modified_at: string;
5
- size: string;
6
- digest: string;
7
- type: string;
8
- description: string;
9
- tags: string[];
10
- capabilities: string[];
11
- parameters: string;
12
- details: {
13
- parent_model: string;
14
- format: string;
15
- family: string;
16
- families: string[];
17
- parameter_size: string;
18
- quantization_level: string;
19
- };
20
- }