pi-llama-cpp 0.13.0 → 0.15.0

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.
Files changed (77) hide show
  1. package/README.md +19 -18
  2. package/package.json +3 -3
  3. package/src/api/client.ts +25 -0
  4. package/src/constants.ts +2 -2
  5. package/src/enums/status.ts +0 -1
  6. package/src/index.ts +2 -1
  7. package/src/interfaces/endpoints/models.ts +1 -1
  8. package/src/interfaces/settings.ts +7 -1
  9. package/src/interfaces/sortBy.ts +4 -0
  10. package/src/managers/command/models.ts +254 -0
  11. package/src/managers/command.ts +34 -461
  12. package/src/managers/events.ts +2 -2
  13. package/src/managers/server.ts +34 -18
  14. package/src/managers/settings.ts +26 -90
  15. package/src/models/baseModel.ts +27 -10
  16. package/src/models/legacyModel.ts +2 -2
  17. package/src/models/routerModel.ts +2 -2
  18. package/src/models/singleModel.ts +1 -1
  19. package/src/server.ts +17 -38
  20. package/src/sse/client.ts +113 -59
  21. package/src/sse/fetch.ts +43 -0
  22. package/src/sse/manager.ts +9 -27
  23. package/src/sse/types.ts +0 -4
  24. package/src/ui/dialog/base.ts +118 -0
  25. package/src/ui/dialog/confirm.ts +45 -0
  26. package/src/ui/dialog/factory.ts +111 -0
  27. package/src/ui/dialog/input.ts +63 -0
  28. package/src/ui/dialog/options.ts +23 -0
  29. package/src/ui/editors/editorOptions.ts +50 -0
  30. package/src/ui/editors/itemBuilder.ts +43 -0
  31. package/src/ui/editors/listEditor.ts +300 -0
  32. package/src/ui/editors/override/entry.ts +24 -0
  33. package/src/ui/editors/override/entryEditor.ts +248 -0
  34. package/src/ui/editors/override/fields/base.ts +53 -0
  35. package/src/ui/editors/override/fields/capabilities.ts +36 -0
  36. package/src/ui/editors/override/fields/cost.ts +64 -0
  37. package/src/ui/editors/override/fields/index.ts +68 -0
  38. package/src/ui/editors/override/fields/numeric.ts +55 -0
  39. package/src/ui/editors/override/fields/pattern.ts +24 -0
  40. package/src/ui/editors/override/fields/reasoning.ts +33 -0
  41. package/src/ui/editors/override/itemBuilder.ts +39 -0
  42. package/src/ui/editors/override/overrideList.ts +127 -0
  43. package/src/ui/editors/server/builder.ts +60 -0
  44. package/src/ui/editors/server/fields.ts +84 -0
  45. package/src/ui/editors/server/itemBuilder.ts +64 -0
  46. package/src/ui/editors/server/serverEditor.ts +197 -0
  47. package/src/ui/editors/server/utils.ts +95 -0
  48. package/src/ui/editors/server/wizard.ts +110 -0
  49. package/src/ui/editors/settingField.ts +37 -0
  50. package/src/ui/editors/settingsListFactory.ts +33 -0
  51. package/src/ui/settings/index.ts +248 -0
  52. package/src/ui/strings.ts +25 -4
  53. package/src/utils/health.ts +2 -1
  54. package/src/utils/serverIds.ts +21 -0
  55. package/src/utils/settingsStore.ts +1 -1
  56. package/src/utils/urlResolver.ts +129 -0
  57. package/src/utils/urls.ts +33 -13
  58. package/tests/{commandManager.test.ts → command/commandManager.test.ts} +88 -12
  59. package/tests/{events.test.ts → events/events.test.ts} +6 -6
  60. package/tests/mocks.ts +2 -0
  61. package/tests/models/legacyModel.test.ts +103 -0
  62. package/tests/{routerModel.test.ts → models/routerModel.test.ts} +71 -72
  63. package/tests/{singleModel.test.ts → models/singleModel.test.ts} +17 -17
  64. package/tests/{health.test.ts → server/health.test.ts} +2 -2
  65. package/tests/{server.test.ts → server/server.test.ts} +5 -17
  66. package/tests/{serverManager.test.ts → server/serverManager.test.ts} +4 -4
  67. package/tests/{settings.test.ts → settings/settings.test.ts} +184 -141
  68. package/tests/{settingsStore.test.ts → settings/settingsStore.test.ts} +1 -1
  69. package/tests/{sseManager.test.ts → sse/sseManager.test.ts} +8 -26
  70. package/tests/{dialog.test.ts → ui/dialog.test.ts} +56 -4
  71. package/tests/{overrides.test.ts → ui/overrides.test.ts} +101 -111
  72. package/src/ui/dialog.ts +0 -290
  73. package/src/ui/overrideEntryEditor.ts +0 -119
  74. package/src/ui/overrideSettingsList.ts +0 -710
  75. package/src/ui/serverListEditor.ts +0 -59
  76. package/src/ui/serverSettingsList.ts +0 -513
  77. package/tests/legacyModel.test.ts +0 -97
@@ -1,59 +0,0 @@
1
- import { PROVIDER_PREFIX } from "../constants";
2
- import { ServerStatus } from "../enums/serverStatus";
3
- import type { LlamaServer } from "../interfaces/settings";
4
- import { checkServerHealth } from "../utils/health";
5
- import { normalizeUrl } from "../utils/urls";
6
-
7
- /**
8
- * Validates and normalizes a user-entered server URL: trims whitespace and
9
- * strips trailing slashes (shared core in `utils/urls` — the same treatment
10
- * the settings parser applies), then rejects empty strings, semicolons (the
11
- * settings parser splits on them — use separate entries) and values without
12
- * an http(s) scheme.
13
- *
14
- * @returns The normalized URL, or `null` when the input is invalid
15
- */
16
- export const normalizeServerUrl = (raw: string): string | null => {
17
- const url = normalizeUrl(raw);
18
- if (url.length === 0 || url.includes(";") || !/^https?:\/\//i.test(url)) {
19
- return null;
20
- }
21
- return url;
22
- };
23
-
24
- /**
25
- * Formats the dim `(...)` suffix shown after a server's URL:
26
- * `(<id> - <name>)`, with the auto-detected URL-based id used when no
27
- * custom id override exists (mirrors `Server.providerId`). Returns the
28
- * empty string when there is nothing to show.
29
- */
30
- export const formatServerSuffix = (server: LlamaServer): string => {
31
- if (!server.id && !server.name) return "";
32
- const id = server.id ?? `${PROVIDER_PREFIX}=${server.url}`;
33
- return server.name ? `(${id} - ${server.name})` : `(${server.id})`;
34
- };
35
-
36
- /**
37
- * Emoji indicators for server health status.
38
- */
39
- const SERVER_STATUS_ICONS: Record<ServerStatus, string> = {
40
- [ServerStatus.READY]: "🟢",
41
- [ServerStatus.TIMEOUT]: "🟡",
42
- [ServerStatus.UNREACHABLE]: "🔴",
43
- };
44
-
45
- /**
46
- * Checks the health of a server and returns the corresponding emoji.
47
- *
48
- * Delegates the probe/classification to the shared `checkServerHealth`
49
- * (`utils/health`) — this wrapper only maps the status to its icon.
50
- *
51
- * @param url - The server URL to check
52
- * @param timeout - Maximum time (ms) to wait for the health check
53
- * @returns The health emoji for the server status
54
- */
55
- export const getServerHealthEmoji = async (
56
- url: string,
57
- timeout: number,
58
- ): Promise<string> =>
59
- SERVER_STATUS_ICONS[await checkServerHealth(url, timeout)];
@@ -1,513 +0,0 @@
1
- import {
2
- getSettingsListTheme,
3
- type Theme,
4
- } from "@earendil-works/pi-coding-agent";
5
- import type {
6
- Component,
7
- Focusable,
8
- KeybindingsManager,
9
- TUI,
10
- } from "@earendil-works/pi-tui";
11
- import { SettingsList, type SettingItem } from "@earendil-works/pi-tui";
12
- import type { LlamaServer } from "../interfaces/settings";
13
- import { ConfirmDialog, InputDialog } from "./dialog";
14
- import {
15
- formatServerSuffix,
16
- getServerHealthEmoji,
17
- normalizeServerUrl,
18
- } from "./serverListEditor";
19
- import { fieldMessage, FIELDS, HINTS, TITLES } from "./strings";
20
-
21
- /**
22
- * Options for the SettingsList-based server editor.
23
- */
24
- export interface ServerSettingsListOptions {
25
- /** TUI instance, used to request re-renders after async persists */
26
- tui: TUI;
27
- /** Theme for dialogs (from the ctx.ui.custom factory) */
28
- theme: Theme;
29
- /** App keybindings manager (injected by ctx.ui.custom) */
30
- keybindings: KeybindingsManager;
31
- /**
32
- * Snapshot of the merged `llamaSettings.servers` to edit. Replaced
33
- * in place (`options.servers = next`) after every successful persist
34
- * so the rebuilt rows reflect the adopted list.
35
- */
36
- servers: LlamaServer[];
37
- /** Persists a new server list; a rejection keeps the current list */
38
- persist: (next: LlamaServer[]) => Promise<void>;
39
- /** Closes the editor (called on Esc in the server list) */
40
- done: () => void;
41
- /** Notifies about persistence errors */
42
- onError: (message: string) => void;
43
- /** Timeout (ms) for health checks. Defaults to 1000ms if not provided. */
44
- serverTimeout?: number;
45
- }
46
-
47
- /**
48
- * Builds the SettingItem for one server's editable fields (URL, id, name).
49
- * Each field is infinite → opens an `InputDialog` submenu; commits are
50
- * reported by the containing `SettingsList`'s `onChange` (field id → value).
51
- */
52
- const buildServerFieldItems = (
53
- server: LlamaServer,
54
- theme: Theme,
55
- tui: TUI,
56
- ): SettingItem[] => [
57
- {
58
- id: "url",
59
- label: FIELDS.serverUrl.label,
60
- description: FIELDS.serverUrl.description,
61
- currentValue: server.url,
62
- submenu: InputDialog.inputSubmenu(
63
- theme,
64
- tui,
65
- TITLES.edit(FIELDS.serverUrl.label),
66
- fieldMessage(FIELDS.serverUrl),
67
- FIELDS.serverUrl.placeholder,
68
- normalizeServerUrl,
69
- ),
70
- },
71
- {
72
- id: "id",
73
- label: FIELDS.providerId.label,
74
- description: FIELDS.providerId.description,
75
- currentValue: server.id ?? "",
76
- submenu: InputDialog.inputSubmenu(
77
- theme,
78
- tui,
79
- TITLES.edit(FIELDS.providerId.label),
80
- fieldMessage(FIELDS.providerId),
81
- FIELDS.providerId.placeholder,
82
- ),
83
- },
84
- {
85
- id: "name",
86
- label: FIELDS.displayName.label,
87
- description: FIELDS.displayName.description,
88
- currentValue: server.name ?? "",
89
- submenu: InputDialog.inputSubmenu(
90
- theme,
91
- tui,
92
- TITLES.edit(FIELDS.displayName.label),
93
- fieldMessage(FIELDS.displayName),
94
- FIELDS.displayName.placeholder,
95
- ),
96
- },
97
- ];
98
-
99
- /**
100
- * Builds the SettingItem for one server row in the top-level list.
101
- * Enter drills into the server's field-edit submenu.
102
- */
103
- const buildServerRow = (
104
- server: LlamaServer,
105
- index: number,
106
- theme: Theme,
107
- tui: TUI,
108
- onChange: (field: string, value: string) => void,
109
- healthEmoji: string,
110
- onSubmenuChange: (open: boolean) => void,
111
- ): SettingItem => ({
112
- id: `server-${index}`,
113
- label: `${healthEmoji} ${server.url}`,
114
- description: HINTS.serverRow,
115
- currentValue: formatServerSuffix(server),
116
- submenu: (_cv, done) => {
117
- // Rebuild the field items on open so they prefill with current values
118
- const items = buildServerFieldItems(server, theme, tui);
119
- onSubmenuChange(true);
120
- return new SettingsList(
121
- items,
122
- Math.min(items.length + 2, 15),
123
- getSettingsListTheme(),
124
- // Field commits arrive here: done(value) from the input submenus
125
- (field, value) => onChange(field, value),
126
- () => {
127
- onSubmenuChange(false);
128
- done();
129
- },
130
- );
131
- },
132
- });
133
-
134
- /**
135
- * Wrapper around a `SettingsList` of servers that adds `a` (add) and `d`
136
- * (delete) support at the list level.
137
- *
138
- * - **a** opens the add wizard — a sequence of framed `InputDialog`s
139
- * (URL → optional ID → optional name, mirroring `/login`'s sequential
140
- * prompts). Esc at any step aborts without persisting; the server is
141
- * saved only after the final step.
142
- * - **d** opens a `ConfirmDialog` (Delete/Cancel select list).
143
- * - **Enter** on a server row drills into its field-edit submenu.
144
- * - **Esc** closes the editor.
145
- */
146
- export class ServerSettingsList implements Component, Focusable {
147
- private settingsList!: SettingsList;
148
- private mode: "list" | "wizard" | "confirm" = "list";
149
- private wizardStep = 0;
150
- private wizardUrl = "";
151
- private wizardId = "";
152
- private wizardDialog: InputDialog | undefined;
153
- private confirmDialog: ConfirmDialog | undefined;
154
- private isFocused = false;
155
- /** True while a server row's field-edit submenu is open */
156
- private submenuOpen = false;
157
-
158
- constructor(private readonly options: ServerSettingsListOptions) {
159
- this.buildSettingsList().then((list) => {
160
- this.settingsList = list;
161
- this.options.tui.requestRender();
162
- });
163
- }
164
-
165
- /**
166
- * Builds the top-level `SettingsList` from the current
167
- * `options.servers`. Called on construction and rebuilt after every
168
- * successful persist so added/deleted/edited servers are reflected
169
- * immediately (the initial snapshot is not mutated by `persist`).
170
- */
171
- private async buildSettingsList(): Promise<SettingsList> {
172
- const { theme, tui } = this.options;
173
- const serverTimeout = this.options.serverTimeout ?? 1000;
174
- // Check health for each server in parallel
175
- const healthEmojis = await Promise.all(
176
- this.options.servers.map((server) =>
177
- getServerHealthEmoji(server.url, serverTimeout),
178
- ),
179
- );
180
-
181
- // A fresh list never has a submenu open (also covers the rebuild
182
- // paths, which discard any open field submenu)
183
- this.submenuOpen = false;
184
- const items: SettingItem[] = this.options.servers.map((server, i) =>
185
- buildServerRow(
186
- server,
187
- i,
188
- theme,
189
- tui,
190
- (field, value) => this.handleFieldChange(field, value),
191
- healthEmojis[i],
192
- (open) => {
193
- this.submenuOpen = open;
194
- },
195
- ),
196
- );
197
-
198
- return new SettingsList(
199
- items,
200
- Math.min(items.length + 2, 15),
201
- getSettingsListTheme(),
202
- () => {
203
- // Top-level onChange is a no-op (servers don't change at this level)
204
- },
205
- () => {
206
- this.options.done();
207
- },
208
- );
209
- }
210
-
211
- // -- Focusable -----------------------------------------------------------
212
-
213
- get focused(): boolean {
214
- return this.isFocused;
215
- }
216
-
217
- set focused(value: boolean) {
218
- this.isFocused = value;
219
- if (this.wizardDialog) this.wizardDialog.focused = value;
220
- if (this.confirmDialog) this.confirmDialog.focused = value;
221
- }
222
-
223
- // -- Component -----------------------------------------------------------
224
-
225
- invalidate(): void {
226
- this.settingsList?.invalidate();
227
- this.wizardDialog?.invalidate();
228
- this.confirmDialog?.invalidate();
229
- }
230
-
231
- handleInput(data: string): void {
232
- if (this.mode === "wizard") {
233
- this.wizardDialog?.handleInput(data);
234
- return;
235
- }
236
-
237
- if (this.mode === "confirm") {
238
- this.confirmDialog?.handleInput(data);
239
- return;
240
- }
241
-
242
- // A server row's field submenu is open: delegate everything (Esc,
243
- // a/d, arrows) to the containing SettingsList, which forwards input
244
- // to the submenu. Intercepting here would close the whole editor on
245
- // Esc and trigger add/delete while typing in a field.
246
- if (this.submenuOpen) {
247
- this.settingsList?.handleInput(data);
248
- return;
249
- }
250
-
251
- const kb = this.options.keybindings;
252
- if (kb.matches(data, "tui.select.cancel")) {
253
- this.options.done();
254
- return;
255
- }
256
- // Track selection for add/delete
257
- if (kb.matches(data, "tui.select.up")) {
258
- this._selectedIndex =
259
- this._selectedIndex === 0
260
- ? this.options.servers.length - 1
261
- : this._selectedIndex - 1;
262
- }
263
- if (kb.matches(data, "tui.select.down")) {
264
- this._selectedIndex =
265
- this._selectedIndex === this.options.servers.length - 1
266
- ? 0
267
- : this._selectedIndex + 1;
268
- }
269
- if (data === "a") {
270
- this.beginWizard();
271
- return;
272
- }
273
- if (data === "d") {
274
- this.beginConfirm();
275
- return;
276
- }
277
- // Delegate to SettingsList (it handles up/down internally for rendering)
278
- this.settingsList?.handleInput(data);
279
- }
280
-
281
- render(width: number): string[] {
282
- if (this.mode === "wizard") {
283
- return this.wizardDialog?.render(width) ?? [];
284
- }
285
-
286
- if (this.mode === "confirm") {
287
- return this.confirmDialog?.render(width) ?? [];
288
- }
289
-
290
- // settingsList may not be initialized yet if buildSettingsList is still
291
- // resolving health checks (async in constructor). Return empty until ready.
292
- if (!this.settingsList) return [];
293
-
294
- // List mode: the shortcuts live in the rows' description; only when the
295
- // list is empty (no rows → no description) show them as the hint line
296
- const settingsLines = this.settingsList.render(width);
297
- if (this.options.servers.length === 0) {
298
- settingsLines[settingsLines.length - 1] = getSettingsListTheme().hint(
299
- HINTS.emptyServers,
300
- );
301
- }
302
- return settingsLines;
303
- }
304
-
305
- // -- helpers ---------------------------------------------------------------
306
-
307
- private selectedIndex(): number {
308
- // Manual tracker for add/delete targeting, kept in sync with the
309
- // SettingsList's cursor by rebuildList (SettingsList doesn't expose
310
- // a getter)
311
- return this._selectedIndex;
312
- }
313
-
314
- private _selectedIndex = 0;
315
-
316
- /**
317
- * Rebuilds the server list and places the cursor on `targetIndex`
318
- * (clamped to the last row). A fresh SettingsList starts its cursor
319
- * at index 0, so `selectItem` is used to restore the position.
320
- */
321
- private async rebuildList(targetIndex: number): Promise<void> {
322
- const list = await this.buildSettingsList();
323
- this.settingsList = list;
324
- const count = this.options.servers.length;
325
- if (count === 0) {
326
- this._selectedIndex = 0;
327
- return;
328
- }
329
- const target = Math.min(targetIndex, count - 1);
330
- this._selectedIndex = target;
331
- this.settingsList.selectItem(`server-${target}`);
332
- }
333
-
334
- // -- add wizard ---------------------------------------------------------------
335
-
336
- /**
337
- * Opens the add wizard at step 1/3 (URL). Esc at any step cancels the
338
- * whole wizard; the server is persisted only after the final step.
339
- */
340
- private beginWizard(): void {
341
- this.mode = "wizard";
342
- this.wizardStep = 0;
343
- this.wizardUrl = "";
344
- this.wizardId = "";
345
- this.showWizardStep();
346
- this.options.tui.requestRender();
347
- }
348
-
349
- private showWizardStep(): void {
350
- const { theme, tui } = this.options;
351
- const step = this.wizardStep;
352
-
353
- const dialog =
354
- step === 0
355
- ? new InputDialog({
356
- theme,
357
- tui,
358
- title: TITLES.addServerStep(1),
359
- message: fieldMessage(FIELDS.serverUrl),
360
- placeholder: FIELDS.serverUrl.placeholder,
361
- validate: normalizeServerUrl,
362
- onSubmit: (value) => this.wizardSubmit(value),
363
- onCancel: () => this.cancelWizard(),
364
- })
365
- : step === 1
366
- ? new InputDialog({
367
- theme,
368
- tui,
369
- title: TITLES.addServerStep(2),
370
- message: fieldMessage(FIELDS.providerId, "(optional)"),
371
- placeholder: FIELDS.providerId.placeholder,
372
- onSubmit: (value) => this.wizardSubmit(value),
373
- onCancel: () => this.cancelWizard(),
374
- })
375
- : new InputDialog({
376
- theme,
377
- tui,
378
- title: TITLES.addServerStep(3),
379
- message: fieldMessage(FIELDS.displayName, "(optional)"),
380
- placeholder: FIELDS.displayName.placeholder,
381
- onSubmit: (value) => this.wizardSubmit(value),
382
- onCancel: () => this.cancelWizard(),
383
- });
384
-
385
- this.wizardDialog = dialog;
386
- dialog.focused = this.isFocused;
387
- }
388
-
389
- private wizardSubmit(value: string): void {
390
- if (this.wizardStep === 0) {
391
- this.wizardUrl = value;
392
- this.wizardStep = 1;
393
- this.showWizardStep();
394
- } else if (this.wizardStep === 1) {
395
- this.wizardId = value;
396
- this.wizardStep = 2;
397
- this.showWizardStep();
398
- } else {
399
- // Final step (display name): commit the collected fields
400
- this.wizardDialog = undefined;
401
- void this.saveNewServer(this.wizardUrl, this.wizardId, value);
402
- return;
403
- }
404
- this.options.tui.requestRender();
405
- }
406
-
407
- private cancelWizard(): void {
408
- this.wizardDialog = undefined;
409
- this.mode = "list";
410
- this.options.tui.requestRender();
411
- }
412
-
413
- private async saveNewServer(
414
- url: string,
415
- id: string,
416
- name: string,
417
- ): Promise<void> {
418
- const next = [
419
- ...this.options.servers,
420
- {
421
- url,
422
- ...(id.length > 0 ? { id } : {}),
423
- ...(name.length > 0 ? { name } : {}),
424
- },
425
- ];
426
- try {
427
- await this.options.persist(next);
428
- // Adopt the persisted list and rebuild so the new row appears;
429
- // the new server is appended last — put the cursor on it
430
- this.options.servers = next;
431
- await this.rebuildList(next.length - 1);
432
- this.mode = "list";
433
- } catch (err) {
434
- this.options.onError(String(err));
435
- this.mode = "list";
436
- }
437
- this.options.tui.requestRender();
438
- }
439
-
440
- // -- delete ------------------------------------------------------------------
441
-
442
- private beginConfirm(): void {
443
- const server = this.options.servers[this.selectedIndex()];
444
- if (!server) return;
445
-
446
- this.mode = "confirm";
447
- this.confirmDialog = new ConfirmDialog({
448
- theme: this.options.theme,
449
- tui: this.options.tui,
450
- title: TITLES.deleteServer,
451
- message: `Delete "${server.url}"?`,
452
- onConfirm: () => {
453
- this.confirmDialog = undefined;
454
- void this.deleteSelected();
455
- },
456
- onCancel: () => {
457
- this.confirmDialog = undefined;
458
- this.mode = "list";
459
- this.options.tui.requestRender();
460
- },
461
- });
462
- this.confirmDialog.focused = this.isFocused;
463
- this.options.tui.requestRender();
464
- }
465
-
466
- private async deleteSelected(): Promise<void> {
467
- const idx = this.selectedIndex();
468
- const servers = this.options.servers;
469
- const next = servers.filter((_, i) => i !== idx);
470
- try {
471
- await this.options.persist(next);
472
- // Adopt the persisted list and rebuild so the row disappears;
473
- // the server that followed the deleted one now sits at the same
474
- // index (rebuildList clamps when the last row was deleted)
475
- this.options.servers = next;
476
- await this.rebuildList(idx);
477
- this.mode = "list";
478
- } catch (err) {
479
- this.options.onError(String(err));
480
- this.mode = "list";
481
- }
482
- this.options.tui.requestRender();
483
- }
484
-
485
- private async handleFieldChange(field: string, value: string): Promise<void> {
486
- const idx = this._selectedIndex;
487
- const server = this.options.servers[idx];
488
- if (!server) return;
489
-
490
- const next = this.options.servers.map((s, i) => {
491
- if (i !== idx) return s;
492
- if (field === "url") return { ...s, url: value };
493
- if (field === "id") {
494
- const { id, ...rest } = s;
495
- return value.length === 0 ? rest : { ...rest, id: value };
496
- }
497
- const { name, ...rest } = s;
498
- return value.length === 0 ? rest : { ...rest, name: value };
499
- });
500
-
501
- try {
502
- await this.options.persist(next);
503
- // URL/id/name changes alter row labels → rebuild the list;
504
- // keep the cursor on the edited server
505
- this.options.servers = next;
506
- await this.rebuildList(idx);
507
- this.options.tui.requestRender();
508
- } catch (err) {
509
- this.options.onError(String(err));
510
- this.options.tui.requestRender();
511
- }
512
- }
513
- }
@@ -1,97 +0,0 @@
1
- import { beforeEach, describe, expect, it } from "vitest";
2
- import { Mode } from "../src/enums/mode";
3
- import { DataProperty } from "../src/interfaces/endpoints/models";
4
- import { LegacyModel } from "../src/models/legacyModel";
5
- import { createMockServer, mockRpc } from "./mocks";
6
-
7
- beforeEach(() => {
8
- mockRpc.mockReset();
9
- });
10
-
11
- const createModel = (extra: Partial<DataProperty> = {}): LegacyModel =>
12
- new LegacyModel(
13
- {
14
- id: "test",
15
- tags: [],
16
- object: "model",
17
- owned_by: "test",
18
- created: Date.now(),
19
- ...extra,
20
- },
21
- createMockServer(),
22
- );
23
-
24
- describe("LegacyModel mode", () => {
25
- it("should always return LEGACY mode", () => {
26
- const model = createModel();
27
- expect(model.mode).toBe(Mode.LEGACY);
28
- });
29
- });
30
-
31
- describe("LegacyModel capabilities", () => {
32
- it("should detect image capability when multimodal is in capabilities", async () => {
33
- mockRpc.mockResolvedValueOnce({ modalities: { vision: true } });
34
-
35
- const model = createModel();
36
- const capabilities = await model.getCapabilities();
37
-
38
- expect(capabilities).toEqual(["text", "image"]);
39
- });
40
-
41
- it("should detect text-only capability when multimodal is not in capabilities", async () => {
42
- mockRpc.mockResolvedValueOnce({ modalities: { vision: false } });
43
-
44
- const model = createModel();
45
- const capabilities = await model.getCapabilities();
46
-
47
- expect(capabilities).toEqual(["text"]);
48
- });
49
-
50
- it("should fall back to text-only when auth fails", async () => {
51
- mockRpc.mockRejectedValue(new Error("401 Unauthorized"));
52
-
53
- const model = createModel();
54
- const capabilities = await model.getCapabilities();
55
-
56
- expect(capabilities).toEqual(["text"]);
57
- });
58
- });
59
-
60
- describe("LegacyModel getContextSize", () => {
61
- it("should use max_model_len when it is non-zero", async () => {
62
- mockRpc.mockResolvedValueOnce({ n_ctx: 4096 });
63
- mockRpc.mockResolvedValueOnce({
64
- data: [{ max_model_len: 8192 }],
65
- });
66
-
67
- const model = createModel();
68
- const ctxSize = await model.getContextSize();
69
-
70
- expect(ctxSize).toBe(8192);
71
- expect(mockRpc).toHaveBeenCalledWith("/v1/models");
72
- });
73
-
74
- it("should fall back to n_ctx when max_model_len is 0", async () => {
75
- mockRpc.mockResolvedValueOnce({ n_ctx: 4096 });
76
- mockRpc.mockResolvedValueOnce({
77
- data: [{ max_model_len: 0 }],
78
- });
79
-
80
- const model = createModel();
81
- const ctxSize = await model.getContextSize();
82
-
83
- expect(ctxSize).toBe(4096);
84
- });
85
-
86
- it("should return DEFAULT_CTX when both values are missing/null", async () => {
87
- mockRpc.mockResolvedValueOnce({});
88
- mockRpc.mockResolvedValueOnce({
89
- data: [{ max_model_len: null }],
90
- });
91
-
92
- const model = createModel();
93
- const ctxSize = await model.getContextSize();
94
-
95
- expect(ctxSize).toBe(128000);
96
- });
97
- });