pi-llama-cpp 0.5.0 → 0.6.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 +58 -27
- package/package.json +5 -4
- package/src/constants.ts +9 -4
- package/src/enums/action.ts +3 -2
- package/src/enums/mode.ts +1 -0
- package/src/enums/status.ts +1 -0
- package/src/index.ts +33 -28
- package/src/interfaces/auth.ts +1 -5
- package/src/interfaces/endpoints/props.ts +1 -0
- package/src/managers/command.ts +290 -0
- package/src/managers/events.ts +63 -0
- package/src/managers/server.ts +71 -0
- package/src/models/baseModel.ts +68 -20
- package/src/models/legacyModel.ts +45 -0
- package/src/models/routerModel.ts +7 -30
- package/src/models/singleModel.ts +9 -6
- package/src/resolver.ts +123 -0
- package/src/server.ts +171 -0
- package/tests/commandManager.test.ts +182 -132
- package/tests/legacyModel.test.ts +112 -0
- package/tests/mocks.ts +97 -0
- package/tests/resolver.test.ts +163 -104
- package/tests/routerModel.test.ts +46 -68
- package/tests/server.test.ts +175 -0
- package/tests/serverManager.test.ts +117 -0
- package/tests/singleModel.test.ts +21 -29
- package/src/commands/models.ts +0 -228
- package/src/events.ts +0 -26
- package/src/manager.ts +0 -93
- package/src/tools/resolver.ts +0 -141
- package/src/tools/retriever.ts +0 -71
- package/tests/handlers.test.ts +0 -164
- package/tests/modelsCommand.test.ts +0 -270
package/tests/mocks.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { vi } from "vitest";
|
|
3
|
+
import { Mode } from "../src/enums/mode";
|
|
4
|
+
import { Status } from "../src/enums/status";
|
|
5
|
+
import { BaseModel } from "../src/models/baseModel";
|
|
6
|
+
import { Server } from "../src/server";
|
|
7
|
+
|
|
8
|
+
/** Shared mock RPC — each test configures it */
|
|
9
|
+
export const mockRpc = vi.fn();
|
|
10
|
+
|
|
11
|
+
/** Default mock server that assumes everything works */
|
|
12
|
+
export const createMockServer = (
|
|
13
|
+
overrides: Partial<Server & { apiKey?: string }> = {},
|
|
14
|
+
): Server => {
|
|
15
|
+
const models: BaseModel[] = [];
|
|
16
|
+
const server: Partial<Server> = {
|
|
17
|
+
baseUrl: "http://127.0.0.1:8080",
|
|
18
|
+
models,
|
|
19
|
+
getApiKey: () => Promise.resolve(overrides.apiKey ?? ""),
|
|
20
|
+
fetchModels: () => mockRpc("/v1/models"),
|
|
21
|
+
fetchModelProps: (modelId: string) =>
|
|
22
|
+
mockRpc(`/props?model=${modelId}&autoload=false`),
|
|
23
|
+
fetchServerHealth: () => mockRpc("/health"),
|
|
24
|
+
fetchServerProps: () => mockRpc("/props?autoload=false"),
|
|
25
|
+
postRequest: (resource: "load" | "unload", model: string) =>
|
|
26
|
+
mockRpc(`/models/${resource}`, { model }),
|
|
27
|
+
isReady: async () => {
|
|
28
|
+
try {
|
|
29
|
+
const r = await mockRpc("/health");
|
|
30
|
+
return r.status === "ok";
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
initialize: async () => {
|
|
36
|
+
const { data } = (await mockRpc("/v1/models")) as {
|
|
37
|
+
data: BaseModel[];
|
|
38
|
+
};
|
|
39
|
+
models.length = 0;
|
|
40
|
+
models.push(...(data ?? []));
|
|
41
|
+
},
|
|
42
|
+
...overrides,
|
|
43
|
+
};
|
|
44
|
+
return server as Server;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** Helper to create a mock BaseModel */
|
|
48
|
+
export const createMockModel = (
|
|
49
|
+
name: string,
|
|
50
|
+
overrides: Partial<BaseModel> = {},
|
|
51
|
+
): BaseModel =>
|
|
52
|
+
({
|
|
53
|
+
name,
|
|
54
|
+
id: name,
|
|
55
|
+
mode: Mode.ROUTER,
|
|
56
|
+
serverUrl: "http://127.0.0.1:8080",
|
|
57
|
+
capabilities: ["text"] as ["text"],
|
|
58
|
+
getStatus: vi.fn().mockResolvedValue(Status.LOADED),
|
|
59
|
+
getContextSize: vi.fn().mockResolvedValue(4096),
|
|
60
|
+
getInfo: vi.fn().mockResolvedValue(`Model: ${name}\nID: ${name}`),
|
|
61
|
+
load: vi.fn().mockResolvedValue(undefined),
|
|
62
|
+
unload: vi.fn().mockResolvedValue(undefined),
|
|
63
|
+
toProviderConfig: vi.fn().mockResolvedValue({}),
|
|
64
|
+
getLabel: vi.fn().mockResolvedValue(name),
|
|
65
|
+
...overrides,
|
|
66
|
+
}) as unknown as BaseModel;
|
|
67
|
+
|
|
68
|
+
/** Create a mock extension context */
|
|
69
|
+
export const createMockCtx = (
|
|
70
|
+
selectFn: (prompt: string, options: string[]) => string | null,
|
|
71
|
+
) => ({
|
|
72
|
+
cwd: "/tmp/test",
|
|
73
|
+
ui: {
|
|
74
|
+
select: vi.fn(selectFn),
|
|
75
|
+
notify: vi.fn(),
|
|
76
|
+
theme: {
|
|
77
|
+
fg: (color: string, text: string) => text,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
modelRegistry: {
|
|
81
|
+
find: vi.fn().mockReturnValue({ id: "test-model-id" }),
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
/** Create a mock Pi instance */
|
|
86
|
+
export const createMockPi = () => ({
|
|
87
|
+
setModel: vi.fn(),
|
|
88
|
+
registerProvider: vi.fn(),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
/** Create a mock Pi context for EventManager */
|
|
92
|
+
export const createMockPiContext = (notifyFn: ReturnType<typeof vi.fn>) =>
|
|
93
|
+
({
|
|
94
|
+
ui: {
|
|
95
|
+
notify: notifyFn,
|
|
96
|
+
},
|
|
97
|
+
}) as any as ExtensionContext;
|
package/tests/resolver.test.ts
CHANGED
|
@@ -2,156 +2,215 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
2
2
|
import {
|
|
3
3
|
API_KEY_PLACEHOLDER,
|
|
4
4
|
DEFAULT_LLAMA_SERVER_URL,
|
|
5
|
-
PROVIDER_ID,
|
|
6
5
|
} from "../src/constants";
|
|
7
6
|
|
|
7
|
+
// Mock getAgentDir before importing resolver
|
|
8
|
+
vi.mock("@earendil-works/pi-coding-agent", () => ({
|
|
9
|
+
getAgentDir: vi.fn().mockReturnValue("/fake/agent/dir"),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock("node:fs/promises", () => ({
|
|
13
|
+
access: vi.fn(),
|
|
14
|
+
constants: { F_OK: 0 },
|
|
15
|
+
readFile: vi.fn(),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
// Import mocked modules
|
|
19
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
20
|
+
import { access, readFile } from "node:fs/promises";
|
|
21
|
+
import { ConfigResolver } from "../src/resolver";
|
|
22
|
+
|
|
8
23
|
describe("URL resolution fallback chain", () => {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
});
|
|
24
|
+
const mockAccess = vi.mocked(access);
|
|
25
|
+
const mockReadFile = vi.mocked(readFile);
|
|
26
|
+
const mockGetAgentDir = vi.mocked(getAgentDir);
|
|
13
27
|
|
|
14
28
|
afterEach(() => {
|
|
15
29
|
delete process.env.LLAMA_SERVER_URL;
|
|
30
|
+
vi.resetModules();
|
|
16
31
|
});
|
|
17
32
|
|
|
18
|
-
|
|
19
|
-
vi.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
vi.clearAllMocks();
|
|
35
|
+
mockGetAgentDir.mockReturnValue("/fake/agent/dir");
|
|
36
|
+
// Default: no files exist
|
|
37
|
+
mockAccess.mockRejectedValue(new Error("ENOENT"));
|
|
38
|
+
mockReadFile.mockResolvedValue("");
|
|
39
|
+
});
|
|
24
40
|
|
|
25
|
-
|
|
26
|
-
const
|
|
41
|
+
it("should return default URL when no config is found", async () => {
|
|
42
|
+
const resolver = new ConfigResolver();
|
|
43
|
+
const result = await resolver.resolveUrls("/tmp/test-project");
|
|
27
44
|
|
|
28
|
-
expect(result).
|
|
45
|
+
expect(result).toEqual([DEFAULT_LLAMA_SERVER_URL]);
|
|
29
46
|
});
|
|
30
47
|
|
|
31
48
|
it("should prioritize project config over env variable", async () => {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
.mockResolvedValue(JSON.stringify({ url: "http://localhost:9999" })),
|
|
41
|
-
}));
|
|
49
|
+
mockAccess.mockImplementation(async (_path: unknown) => {
|
|
50
|
+
if (typeof _path === "string" && _path.includes("llama-server.json"))
|
|
51
|
+
return undefined;
|
|
52
|
+
throw new Error("ENOENT");
|
|
53
|
+
});
|
|
54
|
+
mockReadFile.mockResolvedValue(
|
|
55
|
+
JSON.stringify({ url: "http://localhost:9999" }),
|
|
56
|
+
);
|
|
42
57
|
|
|
43
58
|
process.env.LLAMA_SERVER_URL = "http://env-url:8080";
|
|
44
59
|
|
|
45
|
-
const
|
|
46
|
-
const result = await
|
|
60
|
+
const resolver = new ConfigResolver();
|
|
61
|
+
const result = await resolver.resolveUrls("/tmp/test-project");
|
|
47
62
|
|
|
48
|
-
expect(result).
|
|
63
|
+
expect(result).toEqual(["http://localhost:9999"]);
|
|
49
64
|
});
|
|
50
65
|
|
|
51
66
|
it("should use env variable when no project config exists", async () => {
|
|
52
|
-
vi.doMock("node:fs/promises", () => ({
|
|
53
|
-
access: vi.fn().mockRejectedValue(new Error("ENOENT")),
|
|
54
|
-
constants: { F_OK: 0 },
|
|
55
|
-
readFile: vi.fn().mockResolvedValue(""),
|
|
56
|
-
}));
|
|
57
|
-
|
|
58
67
|
process.env.LLAMA_SERVER_URL = "http://env-url:8080";
|
|
59
68
|
|
|
60
|
-
const
|
|
61
|
-
const result = await
|
|
69
|
+
const resolver = new ConfigResolver();
|
|
70
|
+
const result = await resolver.resolveUrls("/tmp/test-project");
|
|
62
71
|
|
|
63
|
-
expect(result).
|
|
72
|
+
expect(result).toEqual(["http://env-url:8080"]);
|
|
64
73
|
});
|
|
65
74
|
|
|
66
75
|
it("should use global settings when no project config or env exists", async () => {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const { resolveUrl } = await import("../src/tools/resolver");
|
|
81
|
-
const result = await resolveUrl("/tmp/test-project");
|
|
82
|
-
|
|
83
|
-
expect(result).toBe("http://global:8080");
|
|
76
|
+
mockAccess.mockImplementation(async (_path: unknown) => {
|
|
77
|
+
if (typeof _path === "string" && _path.includes("settings.json"))
|
|
78
|
+
return undefined;
|
|
79
|
+
throw new Error("ENOENT");
|
|
80
|
+
});
|
|
81
|
+
mockReadFile.mockResolvedValue(
|
|
82
|
+
JSON.stringify({ llamaServerUrl: "http://global:8080" }),
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const resolver = new ConfigResolver();
|
|
86
|
+
const result = await resolver.resolveUrls("/tmp/test-project");
|
|
87
|
+
|
|
88
|
+
expect(result).toEqual(["http://global:8080"]);
|
|
84
89
|
});
|
|
85
90
|
|
|
86
91
|
it("should strip trailing slashes from resolved URL", async () => {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
92
|
+
mockAccess.mockImplementation(async (_path: unknown) => {
|
|
93
|
+
if (typeof _path === "string" && _path.includes("llama-server.json"))
|
|
94
|
+
return undefined;
|
|
95
|
+
throw new Error("ENOENT");
|
|
96
|
+
});
|
|
97
|
+
mockReadFile.mockResolvedValue(
|
|
98
|
+
JSON.stringify({ url: "http://localhost:8080/" }),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const resolver = new ConfigResolver();
|
|
102
|
+
const result = await resolver.resolveUrls("/tmp/test-project");
|
|
103
|
+
|
|
104
|
+
expect(result).toEqual(["http://localhost:8080"]);
|
|
105
|
+
});
|
|
97
106
|
|
|
98
|
-
|
|
99
|
-
|
|
107
|
+
it("should cache the resolved URL on subsequent calls", async () => {
|
|
108
|
+
mockAccess.mockImplementation(async (_path: unknown) => {
|
|
109
|
+
if (typeof _path === "string" && _path.includes("llama-server.json"))
|
|
110
|
+
return undefined;
|
|
111
|
+
throw new Error("ENOENT");
|
|
112
|
+
});
|
|
113
|
+
mockReadFile.mockResolvedValue(
|
|
114
|
+
JSON.stringify({ url: "http://first:8080" }),
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const resolver = new ConfigResolver();
|
|
118
|
+
const result1 = await resolver.resolveUrls("/tmp/project1");
|
|
119
|
+
const result2 = await resolver.resolveUrls("/tmp/project2");
|
|
120
|
+
|
|
121
|
+
expect(result1).toEqual(["http://first:8080"]);
|
|
122
|
+
expect(result2).toEqual(["http://first:8080"]);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("should handle multiple URLs separated by semicolons", async () => {
|
|
126
|
+
mockAccess.mockImplementation(async (_path: unknown) => {
|
|
127
|
+
if (typeof _path === "string" && _path.includes("llama-server.json"))
|
|
128
|
+
return undefined;
|
|
129
|
+
throw new Error("ENOENT");
|
|
130
|
+
});
|
|
131
|
+
mockReadFile.mockResolvedValue(
|
|
132
|
+
JSON.stringify({ url: "http://first:8080;http://second:9090/" }),
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const resolver = new ConfigResolver();
|
|
136
|
+
const result = await resolver.resolveUrls("/tmp/test-project");
|
|
100
137
|
|
|
101
|
-
expect(result).
|
|
138
|
+
expect(result).toEqual(["http://first:8080", "http://second:9090"]);
|
|
102
139
|
});
|
|
103
140
|
});
|
|
104
141
|
|
|
105
142
|
describe("API key resolution", () => {
|
|
143
|
+
const mockAccess = vi.mocked(access);
|
|
144
|
+
const mockReadFile = vi.mocked(readFile);
|
|
145
|
+
const mockGetAgentDir = vi.mocked(getAgentDir);
|
|
146
|
+
|
|
147
|
+
afterEach(() => {
|
|
148
|
+
vi.resetModules();
|
|
149
|
+
});
|
|
150
|
+
|
|
106
151
|
beforeEach(() => {
|
|
107
152
|
vi.clearAllMocks();
|
|
108
|
-
|
|
153
|
+
mockGetAgentDir.mockReturnValue("/fake/agent/dir");
|
|
154
|
+
mockAccess.mockRejectedValue(new Error("ENOENT"));
|
|
155
|
+
mockReadFile.mockResolvedValue("");
|
|
109
156
|
});
|
|
110
157
|
|
|
111
158
|
it("should return placeholder when auth file does not exist", async () => {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}));
|
|
159
|
+
const resolver = new ConfigResolver();
|
|
160
|
+
const result = await resolver.resolveApiKey(
|
|
161
|
+
"llama-server=http://127.0.0.1:8080",
|
|
162
|
+
);
|
|
117
163
|
|
|
118
|
-
|
|
119
|
-
const result = await resolveApiKey();
|
|
120
|
-
|
|
121
|
-
expect(result).toBe(API_KEY_PLACEHOLDER);
|
|
164
|
+
expect(result).toEqual(API_KEY_PLACEHOLDER);
|
|
122
165
|
});
|
|
123
166
|
|
|
124
167
|
it("should return placeholder when provider key is missing", async () => {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const result = await resolveApiKey();
|
|
137
|
-
|
|
138
|
-
expect(result).toBe(API_KEY_PLACEHOLDER);
|
|
168
|
+
mockAccess.mockResolvedValue(undefined);
|
|
169
|
+
mockReadFile.mockResolvedValue(
|
|
170
|
+
JSON.stringify({ "other-provider": { key: "other-key" } }),
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
const resolver = new ConfigResolver();
|
|
174
|
+
const result = await resolver.resolveApiKey(
|
|
175
|
+
"llama-server=http://127.0.0.1:8080",
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
expect(result).toEqual(API_KEY_PLACEHOLDER);
|
|
139
179
|
});
|
|
140
180
|
|
|
141
181
|
it("should return the provider key when present", async () => {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
182
|
+
mockAccess.mockResolvedValue(undefined);
|
|
183
|
+
mockReadFile.mockResolvedValue(
|
|
184
|
+
JSON.stringify({
|
|
185
|
+
"llama-server=http://127.0.0.1:8080": { key: "test-api-key" },
|
|
186
|
+
}),
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const resolver = new ConfigResolver();
|
|
190
|
+
const result = await resolver.resolveApiKey(
|
|
191
|
+
"llama-server=http://127.0.0.1:8080",
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
expect(result).toEqual("test-api-key");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("should cache the auth file and reuse the key", async () => {
|
|
198
|
+
mockAccess.mockResolvedValue(undefined);
|
|
199
|
+
mockReadFile.mockResolvedValue(
|
|
200
|
+
JSON.stringify({
|
|
201
|
+
"llama-server=http://127.0.0.1:8080": { key: "cached-key" },
|
|
202
|
+
}),
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
const resolver = new ConfigResolver();
|
|
206
|
+
const result1 = await resolver.resolveApiKey(
|
|
207
|
+
"llama-server=http://127.0.0.1:8080",
|
|
208
|
+
);
|
|
209
|
+
const result2 = await resolver.resolveApiKey(
|
|
210
|
+
"llama-server=http://127.0.0.1:8080",
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
expect(result1).toBe("cached-key");
|
|
214
|
+
expect(result2).toBe("cached-key");
|
|
156
215
|
});
|
|
157
216
|
});
|
|
@@ -1,16 +1,8 @@
|
|
|
1
|
-
import { describe, expect, it
|
|
1
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
2
2
|
import { Mode } from "../src/enums/mode";
|
|
3
3
|
import { DataProperty } from "../src/interfaces/endpoints/models";
|
|
4
4
|
import { RouterModel } from "../src/models/routerModel";
|
|
5
|
-
|
|
6
|
-
// Mock the retriever module before importing anything that depends on it
|
|
7
|
-
const mockRpc = vi.fn();
|
|
8
|
-
|
|
9
|
-
vi.mock("../src/tools/retriever", () => ({
|
|
10
|
-
rpc: (...args: unknown[]) => mockRpc(...args),
|
|
11
|
-
isServerReady: vi.fn(),
|
|
12
|
-
listModels: vi.fn(),
|
|
13
|
-
}));
|
|
5
|
+
import { createMockServer, mockRpc } from "./mocks";
|
|
14
6
|
|
|
15
7
|
// Helper to create a mock DataProperty
|
|
16
8
|
const createModel = (overrides: Partial<DataProperty> = {}): DataProperty => ({
|
|
@@ -24,6 +16,10 @@ const createModel = (overrides: Partial<DataProperty> = {}): DataProperty => ({
|
|
|
24
16
|
...overrides,
|
|
25
17
|
});
|
|
26
18
|
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
mockRpc.mockClear();
|
|
21
|
+
});
|
|
22
|
+
|
|
27
23
|
describe("RouterModel context size extraction", () => {
|
|
28
24
|
it("should extract --ctx-size value", () => {
|
|
29
25
|
const model = new RouterModel(
|
|
@@ -41,6 +37,7 @@ describe("RouterModel context size extraction", () => {
|
|
|
41
37
|
preset: "default",
|
|
42
38
|
},
|
|
43
39
|
}),
|
|
40
|
+
createMockServer(),
|
|
44
41
|
);
|
|
45
42
|
|
|
46
43
|
// Access the private method via any
|
|
@@ -57,6 +54,7 @@ describe("RouterModel context size extraction", () => {
|
|
|
57
54
|
preset: "default",
|
|
58
55
|
},
|
|
59
56
|
}),
|
|
57
|
+
createMockServer(),
|
|
60
58
|
);
|
|
61
59
|
|
|
62
60
|
const extractFrom = (model as any).extractFrom.bind(model);
|
|
@@ -72,6 +70,7 @@ describe("RouterModel context size extraction", () => {
|
|
|
72
70
|
preset: "default",
|
|
73
71
|
},
|
|
74
72
|
}),
|
|
73
|
+
createMockServer(),
|
|
75
74
|
);
|
|
76
75
|
|
|
77
76
|
const extractFrom = (model as any).extractFrom.bind(model);
|
|
@@ -88,6 +87,7 @@ describe("RouterModel context size extraction", () => {
|
|
|
88
87
|
preset: "default",
|
|
89
88
|
},
|
|
90
89
|
}),
|
|
90
|
+
createMockServer(),
|
|
91
91
|
);
|
|
92
92
|
|
|
93
93
|
const extractFrom = (model as any).extractFrom.bind(model);
|
|
@@ -103,6 +103,7 @@ describe("RouterModel context size extraction", () => {
|
|
|
103
103
|
preset: "default",
|
|
104
104
|
},
|
|
105
105
|
}),
|
|
106
|
+
createMockServer(),
|
|
106
107
|
);
|
|
107
108
|
|
|
108
109
|
const extractFrom = (model as any).extractFrom.bind(model);
|
|
@@ -110,27 +111,9 @@ describe("RouterModel context size extraction", () => {
|
|
|
110
111
|
});
|
|
111
112
|
|
|
112
113
|
it("should prefer --ctx-size over --fit-ctx when loaded", async () => {
|
|
113
|
-
// First call: getStatus() ->
|
|
114
|
-
mockRpc.mockResolvedValueOnce({
|
|
115
|
-
|
|
116
|
-
{
|
|
117
|
-
id: "test-model",
|
|
118
|
-
status: {
|
|
119
|
-
value: "loaded",
|
|
120
|
-
args: [
|
|
121
|
-
"--model",
|
|
122
|
-
"gguf",
|
|
123
|
-
"--ctx-size",
|
|
124
|
-
"4096",
|
|
125
|
-
"--fit-ctx",
|
|
126
|
-
"8192",
|
|
127
|
-
],
|
|
128
|
-
preset: "default",
|
|
129
|
-
},
|
|
130
|
-
},
|
|
131
|
-
],
|
|
132
|
-
});
|
|
133
|
-
// Second call: super.getContextSize() -> /models with meta.n_ctx
|
|
114
|
+
// First call: getStatus() -> fetchModelProps
|
|
115
|
+
mockRpc.mockResolvedValueOnce({ is_sleeping: false });
|
|
116
|
+
// Second call: super.getContextSize() -> fetchModels with meta.n_ctx
|
|
134
117
|
mockRpc.mockResolvedValueOnce({
|
|
135
118
|
data: [
|
|
136
119
|
{
|
|
@@ -148,6 +131,7 @@ describe("RouterModel context size extraction", () => {
|
|
|
148
131
|
preset: "default",
|
|
149
132
|
},
|
|
150
133
|
}),
|
|
134
|
+
createMockServer(),
|
|
151
135
|
);
|
|
152
136
|
|
|
153
137
|
const ctxSize = await model.getContextSize();
|
|
@@ -155,20 +139,9 @@ describe("RouterModel context size extraction", () => {
|
|
|
155
139
|
});
|
|
156
140
|
|
|
157
141
|
it("should return n_ctx from meta when loaded without context size args", async () => {
|
|
158
|
-
// First call: getStatus() ->
|
|
159
|
-
mockRpc.mockResolvedValueOnce({
|
|
160
|
-
|
|
161
|
-
{
|
|
162
|
-
id: "test-model",
|
|
163
|
-
status: {
|
|
164
|
-
value: "loaded",
|
|
165
|
-
args: ["--model", "gguf"],
|
|
166
|
-
preset: "default",
|
|
167
|
-
},
|
|
168
|
-
},
|
|
169
|
-
],
|
|
170
|
-
});
|
|
171
|
-
// Second call: super.getContextSize() -> /models with meta.n_ctx
|
|
142
|
+
// First call: getStatus() -> fetchModelProps
|
|
143
|
+
mockRpc.mockResolvedValueOnce({ is_sleeping: false });
|
|
144
|
+
// Second call: super.getContextSize() -> fetchModels with meta.n_ctx
|
|
172
145
|
mockRpc.mockResolvedValueOnce({
|
|
173
146
|
data: [
|
|
174
147
|
{
|
|
@@ -186,6 +159,7 @@ describe("RouterModel context size extraction", () => {
|
|
|
186
159
|
preset: "default",
|
|
187
160
|
},
|
|
188
161
|
}),
|
|
162
|
+
createMockServer(),
|
|
189
163
|
);
|
|
190
164
|
|
|
191
165
|
const ctxSize = await model.getContextSize();
|
|
@@ -194,33 +168,34 @@ describe("RouterModel context size extraction", () => {
|
|
|
194
168
|
});
|
|
195
169
|
|
|
196
170
|
describe("RouterModel capabilities detection", () => {
|
|
197
|
-
it("should detect image capability
|
|
198
|
-
mockRpc.mockResolvedValueOnce({
|
|
199
|
-
data: [
|
|
200
|
-
{
|
|
201
|
-
id: "test-model",
|
|
202
|
-
status: {
|
|
203
|
-
value: "loaded",
|
|
204
|
-
args: [],
|
|
205
|
-
preset: "default",
|
|
206
|
-
failed: false,
|
|
207
|
-
},
|
|
208
|
-
architecture: {
|
|
209
|
-
input_modalities: ["text", "image"],
|
|
210
|
-
output_modalities: ["text"],
|
|
211
|
-
},
|
|
212
|
-
},
|
|
213
|
-
],
|
|
214
|
-
});
|
|
171
|
+
it("should detect image capability when modalities.vision is true", async () => {
|
|
172
|
+
mockRpc.mockResolvedValueOnce({ modalities: { vision: true } });
|
|
215
173
|
|
|
216
|
-
const model = new RouterModel(createModel());
|
|
174
|
+
const model = new RouterModel(createModel(), createMockServer());
|
|
217
175
|
const capabilities = await model.getCapabilities();
|
|
218
176
|
|
|
219
177
|
expect(capabilities).toEqual(["text", "image"]);
|
|
220
|
-
expect(mockRpc).toHaveBeenCalledWith(
|
|
178
|
+
expect(mockRpc).toHaveBeenCalledWith(
|
|
179
|
+
"/props?model=test-model&autoload=false",
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("should return text-only when fetchModelProps fails", async () => {
|
|
184
|
+
// First call (fetchModelProps) throws to trigger fallback
|
|
185
|
+
mockRpc.mockRejectedValueOnce(new Error("props not available"));
|
|
186
|
+
// Second call (fetchModels) returns empty data so model is not found
|
|
187
|
+
mockRpc.mockResolvedValueOnce({ data: [] });
|
|
188
|
+
|
|
189
|
+
const model = new RouterModel(createModel(), createMockServer());
|
|
190
|
+
const capabilities = await model.getCapabilities();
|
|
191
|
+
|
|
192
|
+
expect(capabilities).toEqual(["text"]);
|
|
221
193
|
});
|
|
222
194
|
|
|
223
195
|
it("should detect text-only capability when only text in input_modalities", async () => {
|
|
196
|
+
// First call (fetchModelProps) throws to trigger fallback
|
|
197
|
+
mockRpc.mockRejectedValueOnce(new Error("props not available"));
|
|
198
|
+
// Second call (fetchModels) returns the data
|
|
224
199
|
mockRpc.mockResolvedValueOnce({
|
|
225
200
|
data: [
|
|
226
201
|
{
|
|
@@ -239,13 +214,16 @@ describe("RouterModel capabilities detection", () => {
|
|
|
239
214
|
],
|
|
240
215
|
});
|
|
241
216
|
|
|
242
|
-
const model = new RouterModel(createModel());
|
|
217
|
+
const model = new RouterModel(createModel(), createMockServer());
|
|
243
218
|
const capabilities = await model.getCapabilities();
|
|
244
219
|
|
|
245
220
|
expect(capabilities).toEqual(["text"]);
|
|
246
221
|
});
|
|
247
222
|
|
|
248
223
|
it("should return text when model not found in /models response", async () => {
|
|
224
|
+
// First call (fetchModelProps) throws to trigger fallback
|
|
225
|
+
mockRpc.mockRejectedValueOnce(new Error("props not available"));
|
|
226
|
+
// Second call (fetchModels) returns data without matching model
|
|
249
227
|
mockRpc.mockResolvedValueOnce({
|
|
250
228
|
data: [
|
|
251
229
|
{
|
|
@@ -260,7 +238,7 @@ describe("RouterModel capabilities detection", () => {
|
|
|
260
238
|
],
|
|
261
239
|
});
|
|
262
240
|
|
|
263
|
-
const model = new RouterModel(createModel());
|
|
241
|
+
const model = new RouterModel(createModel(), createMockServer());
|
|
264
242
|
const capabilities = await model.getCapabilities();
|
|
265
243
|
|
|
266
244
|
expect(capabilities).toEqual(["text"]);
|
|
@@ -269,7 +247,7 @@ describe("RouterModel capabilities detection", () => {
|
|
|
269
247
|
|
|
270
248
|
describe("RouterModel mode", () => {
|
|
271
249
|
it("should always return ROUTER mode", () => {
|
|
272
|
-
const model = new RouterModel(createModel());
|
|
250
|
+
const model = new RouterModel(createModel(), createMockServer());
|
|
273
251
|
expect(model.mode).toBe(Mode.ROUTER);
|
|
274
252
|
});
|
|
275
253
|
});
|