pi-llama-cpp 0.14.0 → 0.16.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.
- package/README.md +52 -13
- package/package.json +3 -3
- package/src/api/client.ts +59 -22
- package/src/constants.ts +5 -0
- package/src/enums/mode.ts +1 -0
- package/src/index.ts +2 -1
- package/src/interfaces/endpoints/models.ts +6 -0
- package/src/interfaces/endpoints/props.ts +14 -0
- package/src/interfaces/settings.ts +6 -0
- package/src/managers/command/models.ts +12 -6
- package/src/managers/command.ts +6 -1
- package/src/managers/server.ts +30 -38
- package/src/managers/settings.ts +23 -3
- package/src/models/legacyModel.ts +3 -3
- package/src/models/llamaSwapModel.ts +90 -0
- package/src/models/routerModel.ts +2 -2
- package/src/models/singleModel.ts +2 -2
- package/src/server.ts +42 -15
- package/src/sse/manager.ts +2 -2
- package/src/ui/dialog/confirm.ts +1 -1
- package/src/ui/dialog/input.ts +1 -1
- package/src/ui/editors/editorOptions.ts +8 -1
- package/src/ui/editors/itemBuilder.ts +2 -6
- package/src/ui/editors/listEditor.ts +24 -15
- package/src/ui/editors/override/entryEditor.ts +105 -23
- package/src/ui/editors/override/fields/base.ts +53 -0
- package/src/ui/editors/override/fields/capabilities.ts +36 -0
- package/src/ui/editors/override/fields/cost.ts +64 -0
- package/src/ui/editors/override/fields/index.ts +68 -0
- package/src/ui/editors/override/fields/numeric.ts +55 -0
- package/src/ui/editors/override/fields/pattern.ts +24 -0
- package/src/ui/editors/override/fields/reasoning.ts +33 -0
- package/src/ui/editors/override/itemBuilder.ts +4 -4
- package/src/ui/editors/server/fields.ts +2 -2
- package/src/ui/editors/server/itemBuilder.ts +2 -3
- package/src/ui/editors/server/serverEditor.ts +58 -22
- package/src/ui/editors/server/utils.ts +50 -0
- package/src/ui/settings/index.ts +11 -0
- package/src/ui/strings.ts +17 -0
- package/src/utils/credentialResolver.ts +110 -0
- package/src/utils/health.ts +2 -2
- package/src/utils/urlResolver.ts +1 -1
- package/tests/{commandManager.test.ts → command/commandManager.test.ts} +80 -9
- package/tests/{events.test.ts → events/events.test.ts} +6 -6
- package/tests/mocks.ts +4 -0
- package/tests/{legacyModel.test.ts → models/legacyModel.test.ts} +5 -5
- package/tests/models/llamaSwapModel.test.ts +327 -0
- package/tests/{routerModel.test.ts → models/routerModel.test.ts} +5 -5
- package/tests/{singleModel.test.ts → models/singleModel.test.ts} +5 -5
- package/tests/{health.test.ts → server/health.test.ts} +4 -4
- package/tests/{server.test.ts → server/server.test.ts} +57 -7
- package/tests/{serverManager.test.ts → server/serverManager.test.ts} +4 -4
- package/tests/{settings.test.ts → settings/settings.test.ts} +198 -355
- package/tests/{settingsStore.test.ts → settings/settingsStore.test.ts} +1 -1
- package/tests/{sseManager.test.ts → sse/sseManager.test.ts} +2 -2
- package/tests/{dialog.test.ts → ui/dialog.test.ts} +5 -4
- package/tests/{overrides.test.ts → ui/overrides.test.ts} +9 -42
- package/tests/utils/credentialResolver.test.ts +66 -0
- package/tests/utils/urlResolver.test.ts +116 -0
- package/src/ui/editors/override/fields.ts +0 -294
- package/src/ui/editors/override/handlers.ts +0 -118
- package/src/ui/editors/server/handlers.ts +0 -32
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
import { API_KEY_PLACEHOLDER } from "../constants";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolves credential API keys from `auth.json` entries.
|
|
7
|
+
*
|
|
8
|
+
* Supports four formats:
|
|
9
|
+
* - **Literal** — `"sk-abc123"` used as-is
|
|
10
|
+
* - **Shell command** — `"!cat ~/.secrets/key"` executes and captures stdout
|
|
11
|
+
* - **Env ref** — `"$VAR"` or `"${VAR}"` resolved from `credential.env` then `process.env`
|
|
12
|
+
* - **Escape** — `"$$literal"` → `"$literal"`, `"$!bang"` → `"!bang"`
|
|
13
|
+
*/
|
|
14
|
+
export class CredentialResolver {
|
|
15
|
+
/**
|
|
16
|
+
* Resolves a credential key to its actual value.
|
|
17
|
+
*
|
|
18
|
+
* @param key - The raw key string from the credential
|
|
19
|
+
* @param env - Optional env map from `credential.env`. Checked before `process.env`.
|
|
20
|
+
* @returns The resolved API key value, or the placeholder on failure.
|
|
21
|
+
*/
|
|
22
|
+
resolve(key: string, env?: Record<string, string>): string {
|
|
23
|
+
if (!key) return API_KEY_PLACEHOLDER;
|
|
24
|
+
|
|
25
|
+
if (key.startsWith("!")) return this.resolveShellCommand(key);
|
|
26
|
+
if (key.startsWith("$$")) return this.resolveEscape(key);
|
|
27
|
+
if (key.startsWith("$!")) return this.resolveEscape(key);
|
|
28
|
+
if (!key.startsWith("$")) return key;
|
|
29
|
+
|
|
30
|
+
return this.resolveEnvRef(key, env) ?? API_KEY_PLACEHOLDER;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Executes a shell command and returns its trimmed stdout.
|
|
35
|
+
*
|
|
36
|
+
* Strips the leading `!` and runs the remainder as a shell command.
|
|
37
|
+
* On failure (non-zero exit or exception), returns the placeholder.
|
|
38
|
+
*
|
|
39
|
+
* @param command - The full key string starting with `!` (e.g. `"!cat ~/.secrets/key"`).
|
|
40
|
+
* @returns The trimmed stdout, or the API key placeholder on failure.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* resolveShellCommand("!echo my-secret") // → "my-secret"
|
|
45
|
+
* resolveShellCommand("!cat ~/.key") // → contents of file
|
|
46
|
+
* resolveShellCommand("!invalid/cmd") // → API_KEY_PLACEHOLDER
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
private resolveShellCommand(command: string): string {
|
|
50
|
+
try {
|
|
51
|
+
return (
|
|
52
|
+
execSync(command.slice(1), {
|
|
53
|
+
encoding: "utf-8",
|
|
54
|
+
timeout: 10_000,
|
|
55
|
+
}).trim() || API_KEY_PLACEHOLDER
|
|
56
|
+
);
|
|
57
|
+
} catch {
|
|
58
|
+
return API_KEY_PLACEHOLDER;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Resolves escape sequences: `$$` → literal `$`, `$!` → literal `!`.
|
|
64
|
+
*
|
|
65
|
+
* Replaces the leading escape marker with the literal character and
|
|
66
|
+
* preserves any remaining text.
|
|
67
|
+
*
|
|
68
|
+
* @param key - A key string starting with `$$` or `$!`.
|
|
69
|
+
* @returns The literal character followed by the rest of the string.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```ts
|
|
73
|
+
* resolveEscape("$$literal") // → "$literal"
|
|
74
|
+
* resolveEscape("$!bang") // → "!bang"
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
private resolveEscape(key: string): string {
|
|
78
|
+
return key.charAt(1) + key.slice(2);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Resolves `$VAR` or `${VAR}` syntax to the corresponding environment value.
|
|
83
|
+
*
|
|
84
|
+
* Matches the entire key against `$VAR` or `${VAR}` patterns. Looks up the
|
|
85
|
+
* variable first in the provided `env` map, then falls back to `process.env`.
|
|
86
|
+
*
|
|
87
|
+
* @param key - The key string containing a `$VAR` or `${VAR}` reference.
|
|
88
|
+
* @param env - Optional env map (e.g. from `credential.env`). Checked before `process.env`.
|
|
89
|
+
* @returns The resolved environment value, or `undefined` if the var is not found or the format is invalid.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```ts
|
|
93
|
+
* resolveEnvRef("$API_KEY", { API_KEY: "abc" }) // → "abc"
|
|
94
|
+
* resolveEnvRef("${API_KEY}", process.env) // → process.env.API_KEY
|
|
95
|
+
* resolveEnvRef("$UNSET") // → undefined
|
|
96
|
+
* resolveEnvRef("$invalid-var!") // → undefined (invalid name)
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
private resolveEnvRef(
|
|
100
|
+
key: string,
|
|
101
|
+
env?: Record<string, string>,
|
|
102
|
+
): string | undefined {
|
|
103
|
+
const match =
|
|
104
|
+
key.match(/^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/) ??
|
|
105
|
+
key.match(/^\$([A-Za-z_][A-Za-z0-9_]*)$/);
|
|
106
|
+
const varName = match?.[1];
|
|
107
|
+
if (!varName) return undefined;
|
|
108
|
+
return env?.[varName] ?? process.env[varName];
|
|
109
|
+
}
|
|
110
|
+
}
|
package/src/utils/health.ts
CHANGED
|
@@ -34,8 +34,8 @@ export const checkServerHealth = async (
|
|
|
34
34
|
signal: AbortSignal.timeout(timeout),
|
|
35
35
|
headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : undefined,
|
|
36
36
|
});
|
|
37
|
-
|
|
38
|
-
return
|
|
37
|
+
|
|
38
|
+
return response.ok ? ServerStatus.READY : ServerStatus.UNREACHABLE;
|
|
39
39
|
} catch (error) {
|
|
40
40
|
// `AbortSignal.timeout` rejects `fetch` with a `TimeoutError`
|
|
41
41
|
// DOMException (some runtimes surface it as `AbortError` with a
|
package/src/utils/urlResolver.ts
CHANGED
|
@@ -2,17 +2,17 @@ import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import { initTheme } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import type { KeybindingsManager, TUI } from "@earendil-works/pi-tui";
|
|
4
4
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
-
import { Action } from "
|
|
6
|
-
import { CommandManager } from "
|
|
7
|
-
import { ServerManager } from "
|
|
8
|
-
import type { LlamaSettingsManager } from "
|
|
9
|
-
import { ServerSettingsList } from "
|
|
10
|
-
import { ServerDisplay } from "
|
|
5
|
+
import { Action } from "../../src/enums/action";
|
|
6
|
+
import { CommandManager } from "../../src/managers/command";
|
|
7
|
+
import { ServerManager } from "../../src/managers/server";
|
|
8
|
+
import type { LlamaSettingsManager } from "../../src/managers/settings";
|
|
9
|
+
import { ServerSettingsList } from "../../src/ui/editors/server/serverEditor";
|
|
10
|
+
import { ServerDisplay } from "../../src/ui/editors/server/utils";
|
|
11
11
|
import {
|
|
12
12
|
applySettingChange,
|
|
13
13
|
buildSettingsItems,
|
|
14
14
|
formatMs,
|
|
15
|
-
} from "
|
|
15
|
+
} from "../../src/ui/settings";
|
|
16
16
|
import {
|
|
17
17
|
createMockCtx,
|
|
18
18
|
createMockModel,
|
|
@@ -20,14 +20,15 @@ import {
|
|
|
20
20
|
createMockServer,
|
|
21
21
|
makeSettingsStub,
|
|
22
22
|
mockRpc,
|
|
23
|
-
} from "
|
|
23
|
+
} from "../mocks";
|
|
24
24
|
|
|
25
25
|
beforeEach(() => {
|
|
26
26
|
initTheme();
|
|
27
27
|
vi.clearAllMocks();
|
|
28
28
|
mockRpc.mockResolvedValue({ data: [] });
|
|
29
|
-
// Mock health checks to return
|
|
29
|
+
// Mock health checks and auth probes to return immediately
|
|
30
30
|
vi.spyOn(ServerDisplay, "healthEmoji").mockResolvedValue("🟢");
|
|
31
|
+
vi.spyOn(ServerDisplay, "authEmoji").mockResolvedValue("");
|
|
31
32
|
});
|
|
32
33
|
|
|
33
34
|
/**
|
|
@@ -163,6 +164,7 @@ describe("CommandManager", () => {
|
|
|
163
164
|
"sortBy",
|
|
164
165
|
"pollingTimeout",
|
|
165
166
|
"serverTimeout",
|
|
167
|
+
"showServerUrls",
|
|
166
168
|
]);
|
|
167
169
|
// Booleans are displayed as on/off
|
|
168
170
|
expect(items[0].values).toEqual(["on", "off"]);
|
|
@@ -203,6 +205,18 @@ describe("CommandManager", () => {
|
|
|
203
205
|
"serverTimeout",
|
|
204
206
|
500,
|
|
205
207
|
);
|
|
208
|
+
|
|
209
|
+
await applySettingChange("showServerUrls", "on", settingsStub);
|
|
210
|
+
expect(settingsStub.setLlamaSetting).toHaveBeenCalledWith(
|
|
211
|
+
"showServerUrls",
|
|
212
|
+
true,
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
await applySettingChange("showServerUrls", "off", settingsStub);
|
|
216
|
+
expect(settingsStub.setLlamaSetting).toHaveBeenCalledWith(
|
|
217
|
+
"showServerUrls",
|
|
218
|
+
false,
|
|
219
|
+
);
|
|
206
220
|
});
|
|
207
221
|
});
|
|
208
222
|
|
|
@@ -564,5 +578,62 @@ describe("CommandManager", () => {
|
|
|
564
578
|
expect(choices).toHaveLength(2);
|
|
565
579
|
expect(choices[1]).toContain("model-b");
|
|
566
580
|
});
|
|
581
|
+
|
|
582
|
+
it("should omit server URLs when showServerUrls is false", async () => {
|
|
583
|
+
const models = [createMockModel("model-a")];
|
|
584
|
+
const mockPi = createMockPi();
|
|
585
|
+
const servers = models.map((model) =>
|
|
586
|
+
createMockServer({
|
|
587
|
+
baseUrl: model.serverUrl,
|
|
588
|
+
models: [model],
|
|
589
|
+
}),
|
|
590
|
+
);
|
|
591
|
+
const settingsStub = makeSettingsStub({
|
|
592
|
+
resolveServers: vi.fn(async () => servers),
|
|
593
|
+
resolveShowServerUrls: vi.fn(async () => false),
|
|
594
|
+
});
|
|
595
|
+
const serverManager = new ServerManager(settingsStub);
|
|
596
|
+
const commandManager = new CommandManager(serverManager, settingsStub);
|
|
597
|
+
|
|
598
|
+
const ctx = createMockCtx((prompt, options) => {
|
|
599
|
+
if (prompt.includes("models")) return options[0];
|
|
600
|
+
return Action.INFO;
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
await commandManager.handleCommand("", ctx as any, mockPi as any);
|
|
604
|
+
|
|
605
|
+
const choices = vi.mocked(ctx.ui.select).mock.calls[0][1] as string[];
|
|
606
|
+
expect(choices).toHaveLength(1);
|
|
607
|
+
expect(choices[0]).toBe("model-a ");
|
|
608
|
+
expect(choices[0]).not.toContain("[Server:");
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it("should include server URLs when showServerUrls is true", async () => {
|
|
612
|
+
const models = [createMockModel("model-a")];
|
|
613
|
+
const mockPi = createMockPi();
|
|
614
|
+
const servers = models.map((model) =>
|
|
615
|
+
createMockServer({
|
|
616
|
+
baseUrl: model.serverUrl,
|
|
617
|
+
models: [model],
|
|
618
|
+
}),
|
|
619
|
+
);
|
|
620
|
+
const settingsStub = makeSettingsStub({
|
|
621
|
+
resolveServers: vi.fn(async () => servers),
|
|
622
|
+
resolveShowServerUrls: vi.fn(async () => true),
|
|
623
|
+
});
|
|
624
|
+
const serverManager = new ServerManager(settingsStub);
|
|
625
|
+
const commandManager = new CommandManager(serverManager, settingsStub);
|
|
626
|
+
|
|
627
|
+
const ctx = createMockCtx((prompt, options) => {
|
|
628
|
+
if (prompt.includes("models")) return options[0];
|
|
629
|
+
return Action.INFO;
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
await commandManager.handleCommand("", ctx as any, mockPi as any);
|
|
633
|
+
|
|
634
|
+
const choices = vi.mocked(ctx.ui.select).mock.calls[0][1] as string[];
|
|
635
|
+
expect(choices).toHaveLength(1);
|
|
636
|
+
expect(choices[0]).toContain("[Server: http://127.0.0.1:8080]");
|
|
637
|
+
});
|
|
567
638
|
});
|
|
568
639
|
});
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { THINKING_BUDGETS } from "
|
|
3
|
-
import { Status } from "
|
|
4
|
-
import { EventManager } from "
|
|
5
|
-
import { ServerManager } from "
|
|
6
|
-
import type { Server } from "
|
|
7
|
-
import { createMockModel, createMockServer, makeSettingsStub } from "
|
|
2
|
+
import { THINKING_BUDGETS } from "../../src/constants";
|
|
3
|
+
import { Status } from "../../src/enums/status";
|
|
4
|
+
import { EventManager } from "../../src/managers/events";
|
|
5
|
+
import { ServerManager } from "../../src/managers/server";
|
|
6
|
+
import type { Server } from "../../src/server";
|
|
7
|
+
import { createMockModel, createMockServer, makeSettingsStub } from "../mocks";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Injected settings stub (EventManager and — in the live-list test — the
|
package/tests/mocks.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
POLLING_TIMEOUT,
|
|
8
8
|
REACT_TO_MODEL_SELECT,
|
|
9
9
|
SERVER_TIMEOUT,
|
|
10
|
+
SHOW_SERVER_URLS,
|
|
10
11
|
SORT_BY,
|
|
11
12
|
THINKING_BUDGETS,
|
|
12
13
|
} from "../src/constants";
|
|
@@ -40,6 +41,7 @@ export const makeSettingsStub = (
|
|
|
40
41
|
})),
|
|
41
42
|
resolveServers: vi.fn(async () => []),
|
|
42
43
|
resolveSortBy: vi.fn(async () => SORT_BY),
|
|
44
|
+
resolveShowServerUrls: vi.fn(async () => SHOW_SERVER_URLS),
|
|
43
45
|
resolveApiKey: vi.fn(() => API_KEY_PLACEHOLDER),
|
|
44
46
|
resolveReactToModelSelect: vi.fn(async () => REACT_TO_MODEL_SELECT),
|
|
45
47
|
resolveAutoloadOnMessage: vi.fn(async () => AUTOLOAD_ON_MESSAGE),
|
|
@@ -66,6 +68,8 @@ export const createFakeClients = (): {
|
|
|
66
68
|
get: (endpoint: string) => mockRpc(endpoint),
|
|
67
69
|
post: (endpoint: string, body?: Record<string, unknown>) =>
|
|
68
70
|
mockRpc(endpoint, body),
|
|
71
|
+
rawGet: (endpoint: string) => mockRpc(endpoint),
|
|
72
|
+
rawPost: (endpoint: string) => mockRpc(endpoint),
|
|
69
73
|
clearCache: vi.fn(),
|
|
70
74
|
} as unknown as ApiClient;
|
|
71
75
|
const sseManager = {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it } from "vitest";
|
|
2
|
-
import { FALLBACK_CTX } from "
|
|
3
|
-
import { Mode } from "
|
|
4
|
-
import { DataProperty } from "
|
|
5
|
-
import { LegacyModel } from "
|
|
6
|
-
import { createMockServer, mockRpc } from "
|
|
2
|
+
import { FALLBACK_CTX } from "../../src/constants";
|
|
3
|
+
import { Mode } from "../../src/enums/mode";
|
|
4
|
+
import { DataProperty } from "../../src/interfaces/endpoints/models";
|
|
5
|
+
import { LegacyModel } from "../../src/models/legacyModel";
|
|
6
|
+
import { createMockServer, mockRpc } from "../mocks";
|
|
7
7
|
|
|
8
8
|
beforeEach(() => {
|
|
9
9
|
mockRpc.mockReset();
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { Mode } from "../../src/enums/mode";
|
|
3
|
+
import { Status } from "../../src/enums/status";
|
|
4
|
+
import { LlamaSwapModel } from "../../src/models/llamaSwapModel";
|
|
5
|
+
import { createMockServer, mockRpc } from "../mocks";
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
mockRpc.mockReset();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const createModel = (
|
|
12
|
+
extra: Partial<Record<string, unknown>> = {},
|
|
13
|
+
serverOverrides: Parameters<typeof createMockServer>[0] = {},
|
|
14
|
+
): LlamaSwapModel =>
|
|
15
|
+
new LlamaSwapModel(
|
|
16
|
+
{
|
|
17
|
+
id: "test-model",
|
|
18
|
+
aliases: ["test-alias"],
|
|
19
|
+
tags: [],
|
|
20
|
+
object: "model",
|
|
21
|
+
owned_by: "test",
|
|
22
|
+
created: Date.now(),
|
|
23
|
+
...extra,
|
|
24
|
+
} as any,
|
|
25
|
+
createMockServer({ baseUrl: "http://127.0.0.1:8080", ...serverOverrides }),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
describe("LlamaSwapModel mode", () => {
|
|
29
|
+
it("should always return LLAMASWAP mode", () => {
|
|
30
|
+
const model = createModel();
|
|
31
|
+
expect(model.mode).toBe(Mode.LLAMASWAP);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("LlamaSwapModel capabilities", () => {
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
mockRpc.mockReset().mockResolvedValue({
|
|
38
|
+
data: [
|
|
39
|
+
{
|
|
40
|
+
id: "test-model",
|
|
41
|
+
architecture: {
|
|
42
|
+
input_modalities: ["text", "image"],
|
|
43
|
+
output_modalities: ["text"],
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should detect image capability when input_modalities includes image", async () => {
|
|
51
|
+
const model = createModel();
|
|
52
|
+
const { input } = await model.toProviderConfig();
|
|
53
|
+
|
|
54
|
+
expect(input).toEqual(["text", "image"]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should detect text-only capability when input_modalities only has text", async () => {
|
|
58
|
+
mockRpc.mockResolvedValueOnce({
|
|
59
|
+
data: [
|
|
60
|
+
{
|
|
61
|
+
id: "test-model",
|
|
62
|
+
architecture: {
|
|
63
|
+
input_modalities: ["text"],
|
|
64
|
+
output_modalities: ["text"],
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const model = createModel();
|
|
71
|
+
const { input } = await model.toProviderConfig();
|
|
72
|
+
|
|
73
|
+
expect(input).toEqual(["text"]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("should return text-only when model is not found in fetchModels response", async () => {
|
|
77
|
+
mockRpc.mockResolvedValueOnce({
|
|
78
|
+
data: [
|
|
79
|
+
{
|
|
80
|
+
id: "other-model",
|
|
81
|
+
architecture: {
|
|
82
|
+
input_modalities: ["text", "image"],
|
|
83
|
+
output_modalities: ["text"],
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const model = createModel();
|
|
90
|
+
const { input } = await model.toProviderConfig();
|
|
91
|
+
|
|
92
|
+
expect(input).toEqual(["text"]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("should return text-only when architecture is undefined", async () => {
|
|
96
|
+
mockRpc.mockResolvedValueOnce({
|
|
97
|
+
data: [
|
|
98
|
+
{
|
|
99
|
+
id: "test-model",
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const model = createModel();
|
|
105
|
+
const { input } = await model.toProviderConfig();
|
|
106
|
+
|
|
107
|
+
expect(input).toEqual(["text"]);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("LlamaSwapModel overrides", () => {
|
|
112
|
+
it("should use contextSize override when set", async () => {
|
|
113
|
+
mockRpc.mockResolvedValueOnce({
|
|
114
|
+
data: [{ id: "test-model" }],
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const model = createModel(
|
|
118
|
+
{},
|
|
119
|
+
{
|
|
120
|
+
overrides: { "test-model": { contextSize: 65536 } },
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const { contextWindow } = await model.toProviderConfig();
|
|
125
|
+
expect(contextWindow).toBe(65536);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("should use capabilities override when set", async () => {
|
|
129
|
+
mockRpc.mockResolvedValueOnce({
|
|
130
|
+
data: [{ id: "test-model" }],
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const model = createModel(
|
|
134
|
+
{},
|
|
135
|
+
{
|
|
136
|
+
overrides: { "test-model": { capabilities: ["text"] } },
|
|
137
|
+
},
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const { input } = await model.toProviderConfig();
|
|
141
|
+
expect(input).toEqual(["text"]);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("should fall through to detection when no override matches", async () => {
|
|
145
|
+
const model = createModel(
|
|
146
|
+
{},
|
|
147
|
+
{
|
|
148
|
+
overrides: { "other-model": { contextSize: 65536 } },
|
|
149
|
+
},
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
mockRpc
|
|
153
|
+
.mockResolvedValueOnce({
|
|
154
|
+
data: [
|
|
155
|
+
{
|
|
156
|
+
id: "test-model",
|
|
157
|
+
architecture: {
|
|
158
|
+
input_modalities: ["text", "image"],
|
|
159
|
+
output_modalities: ["text"],
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
})
|
|
164
|
+
.mockResolvedValueOnce({
|
|
165
|
+
data: [
|
|
166
|
+
{
|
|
167
|
+
id: "test-model",
|
|
168
|
+
architecture: {
|
|
169
|
+
input_modalities: ["text", "image"],
|
|
170
|
+
output_modalities: ["text"],
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const { input } = await model.toProviderConfig();
|
|
177
|
+
expect(input).toEqual(["text", "image"]);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe("LlamaSwapModel status", () => {
|
|
182
|
+
it("should return LOADED when status.value is 'loaded'", async () => {
|
|
183
|
+
mockRpc.mockResolvedValueOnce({
|
|
184
|
+
data: [
|
|
185
|
+
{
|
|
186
|
+
id: "test-model",
|
|
187
|
+
status: {
|
|
188
|
+
value: "loaded",
|
|
189
|
+
args: [],
|
|
190
|
+
preset: "default",
|
|
191
|
+
failed: false,
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const model = createModel();
|
|
198
|
+
const status = await model.getStatus();
|
|
199
|
+
|
|
200
|
+
expect(status).toBe(Status.LOADED);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("should return UNLOADED when status.value is 'unloaded'", async () => {
|
|
204
|
+
mockRpc.mockResolvedValueOnce({
|
|
205
|
+
data: [
|
|
206
|
+
{
|
|
207
|
+
id: "test-model",
|
|
208
|
+
status: {
|
|
209
|
+
value: "unloaded",
|
|
210
|
+
args: [],
|
|
211
|
+
preset: "default",
|
|
212
|
+
failed: false,
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
],
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const model = createModel();
|
|
219
|
+
const status = await model.getStatus();
|
|
220
|
+
|
|
221
|
+
expect(status).toBe(Status.UNLOADED);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("should return UNLOADED when status.value is something other than 'loaded'", async () => {
|
|
225
|
+
mockRpc.mockResolvedValueOnce({
|
|
226
|
+
data: [
|
|
227
|
+
{
|
|
228
|
+
id: "test-model",
|
|
229
|
+
status: {
|
|
230
|
+
value: "loading",
|
|
231
|
+
args: [],
|
|
232
|
+
preset: "default",
|
|
233
|
+
failed: false,
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const model = createModel();
|
|
240
|
+
const status = await model.getStatus();
|
|
241
|
+
|
|
242
|
+
expect(status).toBe(Status.UNLOADED);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("should return UNLOADED when model is not found in fetchModels response", async () => {
|
|
246
|
+
mockRpc.mockResolvedValueOnce({
|
|
247
|
+
data: [
|
|
248
|
+
{
|
|
249
|
+
id: "other-model",
|
|
250
|
+
status: {
|
|
251
|
+
value: "loaded",
|
|
252
|
+
args: [],
|
|
253
|
+
preset: "default",
|
|
254
|
+
failed: false,
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
],
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const model = createModel();
|
|
261
|
+
const status = await model.getStatus();
|
|
262
|
+
|
|
263
|
+
expect(status).toBe(Status.UNLOADED);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("should return UNLOADED when status is undefined", async () => {
|
|
267
|
+
mockRpc.mockResolvedValueOnce({
|
|
268
|
+
data: [
|
|
269
|
+
{
|
|
270
|
+
id: "test-model",
|
|
271
|
+
},
|
|
272
|
+
],
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
const model = createModel();
|
|
276
|
+
const status = await model.getStatus();
|
|
277
|
+
|
|
278
|
+
expect(status).toBe(Status.UNLOADED);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
describe("LlamaSwapModel load", () => {
|
|
283
|
+
it("should call GET to /upstream/{id} when model is not loaded", async () => {
|
|
284
|
+
const model = createModel();
|
|
285
|
+
// Override getStatus to return UNLOADED so load proceeds
|
|
286
|
+
model.getStatus = vi.fn().mockResolvedValue(Status.UNLOADED);
|
|
287
|
+
|
|
288
|
+
// mockRpc is used by ApiClient; load -> llamaSwapLoad -> apiClient.get
|
|
289
|
+
mockRpc.mockResolvedValue({});
|
|
290
|
+
|
|
291
|
+
await model.load();
|
|
292
|
+
|
|
293
|
+
expect(mockRpc).toHaveBeenCalledWith("/upstream/test-model");
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("should throw when the GET request fails", async () => {
|
|
297
|
+
const model = createModel();
|
|
298
|
+
model.getStatus = vi.fn().mockResolvedValue(Status.UNLOADED);
|
|
299
|
+
|
|
300
|
+
mockRpc.mockRejectedValue(new Error("GET failed"));
|
|
301
|
+
|
|
302
|
+
await expect(model.load()).rejects.toThrow(
|
|
303
|
+
"Model loading failed: test-model",
|
|
304
|
+
);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("should not call fetch when model is already loaded", async () => {
|
|
308
|
+
const model = createModel();
|
|
309
|
+
model.getStatus = vi.fn().mockResolvedValue(Status.LOADED);
|
|
310
|
+
|
|
311
|
+
await model.load();
|
|
312
|
+
|
|
313
|
+
expect(mockRpc).not.toHaveBeenCalled();
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
describe("LlamaSwapModel unload", () => {
|
|
318
|
+
it("should call POST to /api/models/unload/{id}", async () => {
|
|
319
|
+
const model = createModel();
|
|
320
|
+
|
|
321
|
+
mockRpc.mockResolvedValue({});
|
|
322
|
+
|
|
323
|
+
await model.unload();
|
|
324
|
+
|
|
325
|
+
expect(mockRpc).toHaveBeenCalledWith("/api/models/unload/test-model");
|
|
326
|
+
});
|
|
327
|
+
});
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it } from "vitest";
|
|
2
|
-
import { FALLBACK_CTX } from "
|
|
3
|
-
import { Mode } from "
|
|
4
|
-
import { DataProperty } from "
|
|
5
|
-
import { RouterModel } from "
|
|
6
|
-
import { createMockServer, mockRpc } from "
|
|
2
|
+
import { FALLBACK_CTX } from "../../src/constants";
|
|
3
|
+
import { Mode } from "../../src/enums/mode";
|
|
4
|
+
import { DataProperty } from "../../src/interfaces/endpoints/models";
|
|
5
|
+
import { RouterModel } from "../../src/models/routerModel";
|
|
6
|
+
import { createMockServer, mockRpc } from "../mocks";
|
|
7
7
|
|
|
8
8
|
// Helper to create a mock DataProperty
|
|
9
9
|
const createModel = (overrides: Partial<DataProperty> = {}): DataProperty => ({
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it } from "vitest";
|
|
2
|
-
import { Mode } from "
|
|
3
|
-
import { Status } from "
|
|
4
|
-
import { DataProperty } from "
|
|
5
|
-
import { SingleModel } from "
|
|
6
|
-
import { createMockServer, mockRpc } from "
|
|
2
|
+
import { Mode } from "../../src/enums/mode";
|
|
3
|
+
import { Status } from "../../src/enums/status";
|
|
4
|
+
import { DataProperty } from "../../src/interfaces/endpoints/models";
|
|
5
|
+
import { SingleModel } from "../../src/models/singleModel";
|
|
6
|
+
import { createMockServer, mockRpc } from "../mocks";
|
|
7
7
|
|
|
8
8
|
beforeEach(() => {
|
|
9
9
|
mockRpc.mockReset();
|