pi-llama-cpp 0.8.0 → 0.8.1
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/package.json +1 -1
- package/src/sse/client.ts +18 -0
- package/src/sse/manager.ts +10 -4
- package/PLAN.md +0 -349
package/package.json
CHANGED
package/src/sse/client.ts
CHANGED
|
@@ -13,6 +13,8 @@ export class SSEClient {
|
|
|
13
13
|
private subscribers: Map<string, SSECallback> = new Map();
|
|
14
14
|
private connected: boolean = false;
|
|
15
15
|
private reconnecting: boolean = false; // tracks if EventSource auto-reconnect is in progress
|
|
16
|
+
private _onConnectFailed: (() => void) | null = null;
|
|
17
|
+
private _hasReceivedEvents: boolean = false;
|
|
16
18
|
|
|
17
19
|
/**
|
|
18
20
|
* @param sseEndpoint - The full SSE endpoint URL (e.g., "http://127.0.0.1:8080/models/sse")
|
|
@@ -49,6 +51,11 @@ export class SSEClient {
|
|
|
49
51
|
// EventSource will auto-reconnect; we just track state
|
|
50
52
|
this.connected = false;
|
|
51
53
|
this.reconnecting = true;
|
|
54
|
+
|
|
55
|
+
// Notify subscriber if connection fails before any event is received
|
|
56
|
+
if (!this._hasReceivedEvents && this._onConnectFailed) {
|
|
57
|
+
this._onConnectFailed();
|
|
58
|
+
}
|
|
52
59
|
};
|
|
53
60
|
|
|
54
61
|
this.eventSource.onmessage = (event: MessageEvent) => {
|
|
@@ -59,6 +66,7 @@ export class SSEClient {
|
|
|
59
66
|
model: data.model ?? "*",
|
|
60
67
|
data: data.data,
|
|
61
68
|
};
|
|
69
|
+
this._hasReceivedEvents = true;
|
|
62
70
|
this.dispatch(sseEvent);
|
|
63
71
|
} catch {
|
|
64
72
|
// Invalid JSON, ignore
|
|
@@ -79,6 +87,16 @@ export class SSEClient {
|
|
|
79
87
|
return this.connected;
|
|
80
88
|
}
|
|
81
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Sets a callback to be called when the connection fails before
|
|
92
|
+
* any event is received. Useful for rejecting promises early.
|
|
93
|
+
*
|
|
94
|
+
* @param callback - Called once when connection fails
|
|
95
|
+
*/
|
|
96
|
+
setOnConnectFailed(callback: () => void): void {
|
|
97
|
+
this._onConnectFailed = callback;
|
|
98
|
+
}
|
|
99
|
+
|
|
82
100
|
/**
|
|
83
101
|
* Subscribes to SSE events for a specific model.
|
|
84
102
|
* Auto-connects if not already connected.
|
package/src/sse/manager.ts
CHANGED
|
@@ -46,13 +46,12 @@ export class SSEManager {
|
|
|
46
46
|
if (this.sseSupported !== null) return this.sseSupported;
|
|
47
47
|
|
|
48
48
|
try {
|
|
49
|
-
|
|
49
|
+
let url = this.sseEndpoint;
|
|
50
50
|
if (this.apiKey) {
|
|
51
|
-
|
|
51
|
+
url = `${url}?api_key=${encodeURIComponent(this.apiKey)}`;
|
|
52
52
|
}
|
|
53
|
-
const response = await fetch(
|
|
53
|
+
const response = await fetch(url, {
|
|
54
54
|
method: "GET",
|
|
55
|
-
headers,
|
|
56
55
|
signal: AbortSignal.timeout(SERVER_TIMEOUT),
|
|
57
56
|
});
|
|
58
57
|
this.sseSupported =
|
|
@@ -162,6 +161,7 @@ export class SSEManager {
|
|
|
162
161
|
/**
|
|
163
162
|
* Subscribes to SSE status change events for a specific model.
|
|
164
163
|
* Resolves with the final status string once the model reaches a terminal state.
|
|
164
|
+
* Rejects immediately if the connection fails before any event is received.
|
|
165
165
|
*
|
|
166
166
|
* @param modelId - The model ID to subscribe to
|
|
167
167
|
* @returns Promise that resolves with the final status string
|
|
@@ -182,6 +182,12 @@ export class SSEManager {
|
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
184
|
});
|
|
185
|
+
|
|
186
|
+
// Reject immediately if the connection fails before any event is received
|
|
187
|
+
this.sseClient?.setOnConnectFailed(() => {
|
|
188
|
+
clearTimeout(timeout);
|
|
189
|
+
reject(new Error(`SSE connection failed for model: ${modelId}`));
|
|
190
|
+
});
|
|
185
191
|
});
|
|
186
192
|
}
|
|
187
193
|
|
package/PLAN.md
DELETED
|
@@ -1,349 +0,0 @@
|
|
|
1
|
-
# Model Download Feature — Implementation Plan
|
|
2
|
-
|
|
3
|
-
## Overview
|
|
4
|
-
|
|
5
|
-
Add a `/models-download` command that lets users:
|
|
6
|
-
|
|
7
|
-
- **Download** a model by typing a HuggingFace repo string (e.g. `ggml-org/gemma-3-4b-it-GGUF:Q4_K_M`)
|
|
8
|
-
- **Cancel** an in-progress download
|
|
9
|
-
- **Delete** a cached model via `/models-download delete`
|
|
10
|
-
|
|
11
|
-
Download progress is shown in real time via the existing SSE infrastructure. After completion, the user is notified and invited to run `/models` to refresh.
|
|
12
|
-
|
|
13
|
-
Extra documentation of the server's internals can be found in `~/Proyectos/llama-server/README.md`.
|
|
14
|
-
|
|
15
|
-
---
|
|
16
|
-
|
|
17
|
-
## Key Insight
|
|
18
|
-
|
|
19
|
-
When downloading, the model **does not yet exist** in the local model list. The HF repo string itself acts as the model identifier in SSE events. This means:
|
|
20
|
-
|
|
21
|
-
- Download/cancel/delete are **`Server` operations**, not `BaseModel` methods.
|
|
22
|
-
- SSE subscriptions use the raw HF repo string as the `modelId`.
|
|
23
|
-
- No changes to `BaseModel` are needed.
|
|
24
|
-
|
|
25
|
-
## Command Structure
|
|
26
|
-
|
|
27
|
-
The `/models-download` command has three subcommands:
|
|
28
|
-
|
|
29
|
-
| Subcommand | Description |
|
|
30
|
-
| ---------------------------- | ------------------------------ |
|
|
31
|
-
| `/models-download` (no args) | Interactive download flow |
|
|
32
|
-
| `/models-download delete` | Interactive delete flow |
|
|
33
|
-
| `/models-download cancel` | Cancel an in-progress download |
|
|
34
|
-
|
|
35
|
-
Each subcommand is routed by `DownloadManager.handleCommand(args, ctx, pi)`.
|
|
36
|
-
|
|
37
|
-
---
|
|
38
|
-
|
|
39
|
-
## Files to Create
|
|
40
|
-
|
|
41
|
-
### `src/managers/download.ts` — DownloadManager
|
|
42
|
-
|
|
43
|
-
Orchestrates the download, cancel, and delete workflows.
|
|
44
|
-
|
|
45
|
-
```
|
|
46
|
-
class DownloadManager
|
|
47
|
-
├── constructor(serverManager, pi)
|
|
48
|
-
├── handleCommand(args, ctx, pi) → routes to download / delete / cancel flow
|
|
49
|
-
├── handleDownloadFlow(ctx) → interactive: input HF repo → confirm → download with progress
|
|
50
|
-
├── handleDeleteFlow(ctx) → interactive: select deletable model → confirm → delete
|
|
51
|
-
├── handleCancelFlow(ctx) → cancel in-progress download
|
|
52
|
-
└── helpers: selectServer, selectModel
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
**Dependencies:** `ServerManager` (needs `getServers()` accessor), `pi: ExtensionAPI` (for cleanup notifications).
|
|
56
|
-
|
|
57
|
-
---
|
|
58
|
-
|
|
59
|
-
## Files to Modify
|
|
60
|
-
|
|
61
|
-
### 1. `src/server.ts`
|
|
62
|
-
|
|
63
|
-
Add four methods:
|
|
64
|
-
|
|
65
|
-
| Method | HTTP | Purpose |
|
|
66
|
-
| ------------------------ | ------------------------------------ | ----------------------------------------- |
|
|
67
|
-
| `downloadModel(hfRepo)` | `POST /models` | Trigger download with `{ model: hfRepo }` |
|
|
68
|
-
| `deleteModel(modelId)` | `DELETE /models?model={id}` | Remove cached model |
|
|
69
|
-
| `cancelDownload(hfRepo)` | `POST /models/unload` | Cancel in-progress download |
|
|
70
|
-
| `probeSSESupport()` | delegates to `SSEManager.probeSSE()` | Check `/models/sse` availability |
|
|
71
|
-
|
|
72
|
-
**Note:** All four endpoints are confirmed by the llama-server [API documentation](https://github.com/ggml-org/llama.cpp/blob/main/server/README.md). `POST /models/unload` works for both unloading loaded models AND canceling downloads.
|
|
73
|
-
|
|
74
|
-
**Also:** Add `ApiClient.do_delete()` for DELETE requests. Unlike `get`/`post`, `delete` does **not** need caching since it's a mutation. No public `delete()` wrapper is needed (unlike `get`/`post`).
|
|
75
|
-
|
|
76
|
-
```typescript
|
|
77
|
-
// New signature
|
|
78
|
-
async do_delete<T>(endpoint: string): Promise<T>
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
`do_delete` makes a raw DELETE request with `Authorization: Bearer ${this.apiKey}` (same auth pattern as `do_get`/`do_post`).
|
|
82
|
-
|
|
83
|
-
**Note:** No changes to `BaseModel`. Download and delete are server-level operations that work with model IDs/strings directly, not model instances.
|
|
84
|
-
|
|
85
|
-
### 2. `src/managers/server.ts`
|
|
86
|
-
|
|
87
|
-
Add a public accessor (currently `servers` is private):
|
|
88
|
-
|
|
89
|
-
```typescript
|
|
90
|
-
getServers(): Server[] {
|
|
91
|
-
return this.servers;
|
|
92
|
-
}
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
### 3. `src/interfaces/endpoints/models.ts`
|
|
96
|
-
|
|
97
|
-
Add `can_remove` and `need_download` to `DataProperty` (they're already there — confirmed). No changes needed.
|
|
98
|
-
|
|
99
|
-
### 4. `src/index.ts`
|
|
100
|
-
|
|
101
|
-
- Import and instantiate `DownloadManager`
|
|
102
|
-
- Register the `/models-download` command
|
|
103
|
-
- Pass `serverManager` and `pi` to it
|
|
104
|
-
|
|
105
|
-
### 5. `src/sse/manager.ts`
|
|
106
|
-
|
|
107
|
-
Add `subscribeToDownloadStatus(modelId: string): Promise<'success' | 'failed'>` — see implementation details in the SSE Event Mapping section above.
|
|
108
|
-
|
|
109
|
-
### 6. `src/sse/types.ts`
|
|
110
|
-
|
|
111
|
-
Add `download_finished` and `download_failed` to `SSEEventType` (they're already there — confirmed). No changes needed.
|
|
112
|
-
|
|
113
|
-
---
|
|
114
|
-
|
|
115
|
-
## Download Flow (detailed)
|
|
116
|
-
|
|
117
|
-
```
|
|
118
|
-
User: /models-download
|
|
119
|
-
│
|
|
120
|
-
├─ SSE supported? (probeSSESupport)
|
|
121
|
-
│ └─ No → notify error, exit
|
|
122
|
-
│
|
|
123
|
-
├─ Always select server (even if only one) → ctx.ui.select("Select server:", [...])
|
|
124
|
-
│ └─ Format: "Llama.cpp (http://127.0.0.1:8080)"
|
|
125
|
-
│
|
|
126
|
-
├─ ctx.ui.input("Enter HuggingFace model", "user/repo:quant")
|
|
127
|
-
│ └─ Cancelled → exit
|
|
128
|
-
│
|
|
129
|
-
├─ Basic validation (must contain "/")
|
|
130
|
-
│ └─ Invalid → notify error, exit
|
|
131
|
-
│
|
|
132
|
-
├─ ctx.ui.confirm("Confirm Download", "Download <repo>?")
|
|
133
|
-
│ └─ No → exit
|
|
134
|
-
│
|
|
135
|
-
├─ server.downloadModel(hfRepo)
|
|
136
|
-
│ └─ Response: { success: true } or { error: { code, message, type } }
|
|
137
|
-
│ └─ If error → notify error, exit
|
|
138
|
-
│
|
|
139
|
-
├─ Subscribe to SSE progress (hfRepo as modelId)
|
|
140
|
-
│ └─ On each event: ctx.ui.notify("Downloading ... X%", "info")
|
|
141
|
-
│
|
|
142
|
-
├─ Wait for terminal SSE event (download_finished / download_failed)
|
|
143
|
-
│ ├─ Success → notify "Download complete. Run /models to refresh."
|
|
144
|
-
│ └─ Failure → notify "Download failed: <reason>"
|
|
145
|
-
│
|
|
146
|
-
├─ Call GET /models to update the model list
|
|
147
|
-
│
|
|
148
|
-
└─ Cleanup SSE subscription
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
### Download Timeout
|
|
152
|
-
|
|
153
|
-
Downloads can take several minutes. **No timeout** is applied to the download status subscription — the SSE connection will wait indefinitely for `download_finished` or `download_failed`. The `subscribeToProgress` call already handles progress updates in real time.
|
|
154
|
-
|
|
155
|
-
## Delete Flow (detailed)
|
|
156
|
-
|
|
157
|
-
```
|
|
158
|
-
User: /models-download delete
|
|
159
|
-
│
|
|
160
|
-
├─ Always select server (even if only one) → ctx.ui.select("Select server:", [...])
|
|
161
|
-
│ └─ Format: "Llama.cpp (http://127.0.0.1:8080)"
|
|
162
|
-
│
|
|
163
|
-
├─ Fetch fresh model list from server (GET /models)
|
|
164
|
-
│
|
|
165
|
-
├─ Filter models where can_remove === true
|
|
166
|
-
│ └─ None → notify "No deletable models", exit
|
|
167
|
-
│ └─ Note: models still downloading are NOT in the list yet, so they cannot be deleted.
|
|
168
|
-
│
|
|
169
|
-
├─ ctx.ui.select("Select model to delete:", [...])
|
|
170
|
-
│ └─ Cancelled → exit
|
|
171
|
-
│
|
|
172
|
-
├─ ctx.ui.confirm("Confirm Delete", "Delete <name>? This cannot be undone.")
|
|
173
|
-
│ └─ No → exit
|
|
174
|
-
│
|
|
175
|
-
├─ server.deleteModel(modelId)
|
|
176
|
-
│ └─ Response: { success: true }
|
|
177
|
-
│ ├─ Success → notify "Deleted <name>. Run /models to refresh."
|
|
178
|
-
│ └─ Failure → notify error
|
|
179
|
-
│
|
|
180
|
-
└─ Done
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
## Cancel Download
|
|
184
|
-
|
|
185
|
-
The `cancelDownload` method on `Server` sends `POST /models/unload` with the HF repo string. Per the llama-server docs, `POST /models/unload` cancels in-progress downloads (it's the same endpoint used to unload loaded models).
|
|
186
|
-
|
|
187
|
-
Exposed as `/models-download cancel` subcommand. The flow:
|
|
188
|
-
|
|
189
|
-
```
|
|
190
|
-
User: /models-download cancel
|
|
191
|
-
│
|
|
192
|
-
├─ Always select server (even if only one) → ctx.ui.select("Select server:", [...])
|
|
193
|
-
│ └─ Format: "Llama.cpp (http://127.0.0.1:8080)"
|
|
194
|
-
│
|
|
195
|
-
├─ ctx.ui.input("Enter HF repo to cancel", "user/repo:quant")
|
|
196
|
-
│ └─ Cancelled → exit
|
|
197
|
-
│
|
|
198
|
-
├─ server.cancelDownload(hfRepo)
|
|
199
|
-
│ └─ Sends POST /models/unload with { model: hfRepo }
|
|
200
|
-
│
|
|
201
|
-
├─ Notify: "Download cancelled for <repo>."
|
|
202
|
-
│
|
|
203
|
-
└─ Done
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
**Note:** `cancelDownload` is semantically distinct from `downloadModel` even though they use the same endpoint. It should be a separate method for clarity.
|
|
207
|
-
|
|
208
|
-
---
|
|
209
|
-
|
|
210
|
-
## SSE Event Mapping
|
|
211
|
-
|
|
212
|
-
The existing `SSEManager` already handles these event types:
|
|
213
|
-
|
|
214
|
-
| SSE Event | Current handling | Used by |
|
|
215
|
-
| ------------------------------- | ------------------------------------------------------------ | ------------- |
|
|
216
|
-
| `download_progress` | `subscribeToProgress()` parses `done/total` → percentage | Download flow |
|
|
217
|
-
| `status_change` with `progress` | `subscribeToProgress()` parses `value` → percentage + stage | Download flow |
|
|
218
|
-
| `download_finished` | Not explicitly handled yet — need to add terminal resolution | Download flow |
|
|
219
|
-
| `download_failed` | Not explicitly handled yet — need to add terminal rejection | Download flow |
|
|
220
|
-
|
|
221
|
-
**Gap:** `SSEManager.subscribeToStatus()` currently only resolves on `status_change` events with `loaded`/`failed`. For downloads, we need it to also resolve on `download_finished`/`download_failed` events. Two options:
|
|
222
|
-
|
|
223
|
-
- **(A)** Add a new `subscribeToDownloadStatus(modelId)` method that resolves on download terminal events.
|
|
224
|
-
- **(B)** Extend `subscribeToStatus()` to also handle download terminal events.
|
|
225
|
-
|
|
226
|
-
**Recommendation:** Option (A) — keep concerns separated. The download manager calls `subscribeToDownloadStatus()`.
|
|
227
|
-
|
|
228
|
-
**`subscribeToDownloadStatus(modelId)` implementation:**
|
|
229
|
-
|
|
230
|
-
```typescript
|
|
231
|
-
subscribeToDownloadStatus(modelId: string): Promise<'success' | 'failed'> {
|
|
232
|
-
return new Promise((resolve, reject) => {
|
|
233
|
-
// No timeout — downloads can take several minutes.
|
|
234
|
-
// The SSE connection waits indefinitely for a terminal event.
|
|
235
|
-
|
|
236
|
-
this.subscribeToSSE(modelId, (event: SSEEvent) => {
|
|
237
|
-
if (event.event === SSEEventType.download_finished) {
|
|
238
|
-
resolve('success');
|
|
239
|
-
} else if (event.event === SSEEventType.download_failed) {
|
|
240
|
-
reject(new Error(`Download failed for model: ${modelId}`));
|
|
241
|
-
}
|
|
242
|
-
});
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
- Subscribes to SSE for the HF repo string as `modelId`.
|
|
248
|
-
- Resolves with `"success"` on `download_finished`.
|
|
249
|
-
- Rejects with an error on `download_failed`.
|
|
250
|
-
- The event `data` field contains the model identifier string (e.g., `"ggml-org/gemma-3-4b-it-GGUF:Q4_K_M"`), but we don't need to parse it — the `modelId` parameter already identifies the model.
|
|
251
|
-
|
|
252
|
-
**SSE Event Payloads** (from llama-server docs):
|
|
253
|
-
|
|
254
|
-
```json
|
|
255
|
-
// download_finished / download_failed
|
|
256
|
-
{
|
|
257
|
-
"model": "ggml-org/gemma-3-4b-it-GGUF:Q4_K_M"
|
|
258
|
-
}
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
Both events carry the model identifier in `data`. No additional error details — errors are surfaced via the event type itself (`download_failed` vs `download_finished`).
|
|
262
|
-
|
|
263
|
-
```json
|
|
264
|
-
// model_remove — sent after successful DELETE /models
|
|
265
|
-
{
|
|
266
|
-
"model": "ggml-org/gemma-3-4b-it-GGUF:Q4_K_M"
|
|
267
|
-
}
|
|
268
|
-
```
|
|
269
|
-
|
|
270
|
-
The `model_remove` event is sent by the server after a successful delete. The extension doesn't need to handle it explicitly — the user runs `/models` to refresh the list anyway. But it's good to be aware of it in case we want to auto-refresh later.
|
|
271
|
-
|
|
272
|
-
```json
|
|
273
|
-
// models_reload — sent after model list changes (download completes, delete completes)
|
|
274
|
-
{
|
|
275
|
-
"model": "*"
|
|
276
|
-
}
|
|
277
|
-
```
|
|
278
|
-
|
|
279
|
-
The `models_reload` event uses `"model": "*"` (global event). This is sent after downloads complete and after deletes. We don't need to handle it in v1 — the download flow explicitly calls `GET /models` to refresh. But it's useful context for future auto-refresh features.
|
|
280
|
-
|
|
281
|
-
**Note:** `SSEManager.probeSSE()` already exists and is cached. `SSEEventType` already includes `download_finished` and `download_failed`. The `subscribeToProgress()` method already handles `download_progress` events. We just need to add a terminal resolver.
|
|
282
|
-
|
|
283
|
-
---
|
|
284
|
-
|
|
285
|
-
## Notification Types
|
|
286
|
-
|
|
287
|
-
Pi's `ctx.ui.notify()` accepts `"info" | "warning" | "error"`. There is no `"success"` type. Use `"info"` for success messages.
|
|
288
|
-
|
|
289
|
-
---
|
|
290
|
-
|
|
291
|
-
## API Response Formats
|
|
292
|
-
|
|
293
|
-
### `POST /models` (download)
|
|
294
|
-
|
|
295
|
-
**Success:**
|
|
296
|
-
|
|
297
|
-
```json
|
|
298
|
-
{ "success": true }
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
**Error:**
|
|
302
|
-
|
|
303
|
-
```json
|
|
304
|
-
{
|
|
305
|
-
"error": {
|
|
306
|
-
"code": 400,
|
|
307
|
-
"message": "model validation failed, unable to download",
|
|
308
|
-
"type": "invalid_request_error"
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
### `DELETE /models?model={id}` (delete)
|
|
314
|
-
|
|
315
|
-
**Success:**
|
|
316
|
-
|
|
317
|
-
```json
|
|
318
|
-
{ "success": true }
|
|
319
|
-
```
|
|
320
|
-
|
|
321
|
-
### `POST /models/unload` (cancel download)
|
|
322
|
-
|
|
323
|
-
Same endpoint as unload — response format not explicitly documented by llama-server. Likely returns `{ success: true }` or an empty object. We'll treat any non-throwing response as success.
|
|
324
|
-
|
|
325
|
-
---
|
|
326
|
-
|
|
327
|
-
## Server Selection UX
|
|
328
|
-
|
|
329
|
-
In all three flows (download, delete, cancel), the user always selects a server first — even if there's only one server configured. This keeps the UX consistent and future-proofs the feature.
|
|
330
|
-
|
|
331
|
-
```typescript
|
|
332
|
-
// Example: server list labels
|
|
333
|
-
const choices = servers.map((s) => `${PROVIDER_NAME} (${s.baseUrl})`);
|
|
334
|
-
// → ["Llama.cpp (http://127.0.0.1:8080)", "Llama.cpp (http://127.0.0.1:8081)"]
|
|
335
|
-
const choice = await ctx.ui.select("Select server:", choices);
|
|
336
|
-
```
|
|
337
|
-
|
|
338
|
-
---
|
|
339
|
-
|
|
340
|
-
## TODO Comments
|
|
341
|
-
|
|
342
|
-
Add `// TODO: Verify if download/delete needs to be gated to router mode` in:
|
|
343
|
-
|
|
344
|
-
- `DownloadManager.handleCommand()`
|
|
345
|
-
- `Server.downloadModel()`
|
|
346
|
-
- `Server.deleteModel()`
|
|
347
|
-
- `Server.cancelDownload()`
|
|
348
|
-
|
|
349
|
-
---
|