pi-llama-cpp 0.11.0 → 0.13.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 +186 -58
- package/package.json +5 -5
- package/src/constants.ts +8 -0
- package/src/interfaces/server.ts +7 -0
- package/src/interfaces/settings.ts +67 -1
- package/src/managers/command.ts +110 -31
- package/src/managers/events.ts +2 -2
- package/src/managers/server.ts +14 -11
- package/src/managers/settings.ts +120 -45
- package/src/models/baseModel.ts +37 -7
- package/src/models/routerModel.ts +2 -1
- package/src/server.ts +62 -23
- package/src/sse/manager.ts +9 -8
- package/src/ui/dialog.ts +290 -0
- package/src/ui/overrideEntryEditor.ts +119 -0
- package/src/ui/overrideSettingsList.ts +710 -0
- package/src/ui/serverListEditor.ts +19 -441
- package/src/ui/serverSettingsList.ts +513 -0
- package/src/ui/strings.ts +127 -0
- package/src/utils/health.ts +48 -0
- package/src/utils/settingsStore.ts +2 -6
- package/tests/commandManager.test.ts +132 -16
- package/tests/dialog.test.ts +228 -0
- package/tests/events.test.ts +12 -9
- package/tests/health.test.ts +116 -0
- package/tests/legacyModel.test.ts +4 -19
- package/tests/mocks.ts +12 -8
- package/tests/overrides.test.ts +384 -0
- package/tests/server.test.ts +59 -16
- package/tests/serverManager.test.ts +28 -62
- package/tests/settings.test.ts +489 -43
- package/tests/settingsStore.test.ts +0 -19
- package/tests/singleModel.test.ts +32 -0
- package/tests/sseManager.test.ts +4 -4
- package/tests/serverListEditor.test.ts +0 -637
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it } from "vitest";
|
|
2
2
|
import { Mode } from "../src/enums/mode";
|
|
3
|
-
import { Status } from "../src/enums/status";
|
|
4
3
|
import { DataProperty } from "../src/interfaces/endpoints/models";
|
|
5
4
|
import { LegacyModel } from "../src/models/legacyModel";
|
|
6
5
|
import { createMockServer, mockRpc } from "./mocks";
|
|
@@ -47,28 +46,14 @@ describe("LegacyModel capabilities", () => {
|
|
|
47
46
|
|
|
48
47
|
expect(capabilities).toEqual(["text"]);
|
|
49
48
|
});
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
describe("LegacyModel getStatus", () => {
|
|
53
|
-
it("should return LOADED when not sleeping", async () => {
|
|
54
|
-
mockRpc.mockResolvedValueOnce({ is_sleeping: false });
|
|
55
|
-
|
|
56
|
-
const model = createModel();
|
|
57
|
-
const status = await model.getStatus();
|
|
58
49
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
`/props?model=${model.id}&autoload=false`,
|
|
62
|
-
);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("should return SLEEPING when is_sleeping is true", async () => {
|
|
66
|
-
mockRpc.mockResolvedValueOnce({ is_sleeping: true });
|
|
50
|
+
it("should fall back to text-only when auth fails", async () => {
|
|
51
|
+
mockRpc.mockRejectedValue(new Error("401 Unauthorized"));
|
|
67
52
|
|
|
68
53
|
const model = createModel();
|
|
69
|
-
const
|
|
54
|
+
const capabilities = await model.getCapabilities();
|
|
70
55
|
|
|
71
|
-
expect(
|
|
56
|
+
expect(capabilities).toEqual(["text"]);
|
|
72
57
|
});
|
|
73
58
|
});
|
|
74
59
|
|
package/tests/mocks.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
} from "../src/constants";
|
|
13
13
|
import { Mode } from "../src/enums/mode";
|
|
14
14
|
import { Status } from "../src/enums/status";
|
|
15
|
-
import type {
|
|
15
|
+
import type { ModelOverride } from "../src/interfaces/settings";
|
|
16
16
|
import type { LlamaSettingsManager } from "../src/managers/settings";
|
|
17
17
|
import { BaseModel } from "../src/models/baseModel";
|
|
18
18
|
import { Server } from "../src/server";
|
|
@@ -32,18 +32,19 @@ export const makeSettingsStub = (
|
|
|
32
32
|
overrides: Partial<LlamaSettingsManager> = {},
|
|
33
33
|
): LlamaSettingsManager =>
|
|
34
34
|
({
|
|
35
|
-
|
|
35
|
+
getLlamaSettings: vi.fn(async () => ({})),
|
|
36
|
+
getLlamaServers: vi.fn(async () => []),
|
|
37
|
+
resolveTimeouts: vi.fn(async () => ({
|
|
36
38
|
pollingTimeout: POLLING_TIMEOUT,
|
|
37
39
|
serverTimeout: SERVER_TIMEOUT,
|
|
38
40
|
})),
|
|
39
|
-
resolveServers: vi.fn(()
|
|
40
|
-
resolveSortBy: vi.fn(() => SORT_BY),
|
|
41
|
+
resolveServers: vi.fn(async () => []),
|
|
42
|
+
resolveSortBy: vi.fn(async () => SORT_BY),
|
|
41
43
|
resolveApiKey: vi.fn(() => API_KEY_PLACEHOLDER),
|
|
42
|
-
resolveReactToModelSelect: vi.fn(() => REACT_TO_MODEL_SELECT),
|
|
43
|
-
resolveAutoloadOnMessage: vi.fn(() => AUTOLOAD_ON_MESSAGE),
|
|
44
|
+
resolveReactToModelSelect: vi.fn(async () => REACT_TO_MODEL_SELECT),
|
|
45
|
+
resolveAutoloadOnMessage: vi.fn(async () => AUTOLOAD_ON_MESSAGE),
|
|
44
46
|
resolveThinkingLevel: vi.fn(() => undefined),
|
|
45
47
|
resolveThinkingBudgets: vi.fn(() => ({ ...THINKING_BUDGETS })),
|
|
46
|
-
llamaServers: [] as LlamaServer[],
|
|
47
48
|
takeWarnings: vi.fn((): string[] => []),
|
|
48
49
|
setLlamaSetting: vi.fn(() => Promise.resolve()),
|
|
49
50
|
...overrides,
|
|
@@ -102,6 +103,7 @@ export type MockServerOverrides = Partial<
|
|
|
102
103
|
models?: BaseModel[];
|
|
103
104
|
pollingTimeout?: number;
|
|
104
105
|
serverTimeout?: number;
|
|
106
|
+
overrides?: Record<string, ModelOverride>;
|
|
105
107
|
};
|
|
106
108
|
|
|
107
109
|
/**
|
|
@@ -124,6 +126,7 @@ export const createMockServer = (
|
|
|
124
126
|
models,
|
|
125
127
|
pollingTimeout,
|
|
126
128
|
serverTimeout,
|
|
129
|
+
overrides: modelOverrides,
|
|
127
130
|
initialize,
|
|
128
131
|
...members
|
|
129
132
|
} = overrides;
|
|
@@ -131,7 +134,7 @@ export const createMockServer = (
|
|
|
131
134
|
const settings = makeSettingsStub({
|
|
132
135
|
...(apiKey !== undefined && { resolveApiKey: vi.fn(() => apiKey) }),
|
|
133
136
|
...((pollingTimeout !== undefined || serverTimeout !== undefined) && {
|
|
134
|
-
resolveTimeouts: vi.fn(() => ({
|
|
137
|
+
resolveTimeouts: vi.fn(async () => ({
|
|
135
138
|
pollingTimeout: pollingTimeout ?? POLLING_TIMEOUT,
|
|
136
139
|
serverTimeout: serverTimeout ?? SERVER_TIMEOUT,
|
|
137
140
|
})),
|
|
@@ -145,6 +148,7 @@ export const createMockServer = (
|
|
|
145
148
|
baseUrl: baseUrl ?? "http://127.0.0.1:8080",
|
|
146
149
|
customId,
|
|
147
150
|
customName,
|
|
151
|
+
overrides: modelOverrides,
|
|
148
152
|
},
|
|
149
153
|
{
|
|
150
154
|
createApiClient: () => apiClient,
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { FALLBACK_CTX } from "../src/constants";
|
|
3
|
+
import type { LlamaServer, ModelOverride } from "../src/interfaces/settings";
|
|
4
|
+
import { SingleModel } from "../src/models/singleModel";
|
|
5
|
+
import { Server } from "../src/server";
|
|
6
|
+
import {
|
|
7
|
+
addOverrideEntry,
|
|
8
|
+
applyCostFieldValue,
|
|
9
|
+
formatOverrideSummary,
|
|
10
|
+
parseCostValue,
|
|
11
|
+
removeOverrideEntry,
|
|
12
|
+
updateOverrideEntry,
|
|
13
|
+
} from "../src/ui/overrideEntryEditor";
|
|
14
|
+
import { overrideFieldValue } from "../src/ui/overrideSettingsList";
|
|
15
|
+
import { createMockServer, mockRpc } from "./mocks";
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
mockRpc.mockReset();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const createModel = (overrides?: Record<string, ModelOverride>): SingleModel =>
|
|
22
|
+
new SingleModel(
|
|
23
|
+
{
|
|
24
|
+
id: "test",
|
|
25
|
+
tags: [],
|
|
26
|
+
object: "model",
|
|
27
|
+
owned_by: "test",
|
|
28
|
+
created: Date.now(),
|
|
29
|
+
},
|
|
30
|
+
createMockServer({ overrides }),
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
/** Standard server responses: /props for capabilities, /v1/models for n_ctx */
|
|
34
|
+
const mockDetection = (nCtx: number | undefined) =>
|
|
35
|
+
mockRpc.mockImplementation((endpoint: string) => {
|
|
36
|
+
if (endpoint.startsWith("/props")) {
|
|
37
|
+
return Promise.resolve({ modalities: { vision: false } });
|
|
38
|
+
}
|
|
39
|
+
return Promise.resolve({
|
|
40
|
+
data: [{ id: "test", meta: nCtx === undefined ? {} : { n_ctx: nCtx } }],
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// ─── Pattern matching (Server.findOverrideForModel) ───────────────────────
|
|
45
|
+
|
|
46
|
+
describe("Server.findOverrideForModel", () => {
|
|
47
|
+
const makeServer = (overrides: Record<string, ModelOverride>): Server =>
|
|
48
|
+
createMockServer({ overrides });
|
|
49
|
+
|
|
50
|
+
it("should match a model by id prefix", () => {
|
|
51
|
+
const server = makeServer({ lla: { reasoning: false } });
|
|
52
|
+
expect(server.findOverrideForModel("llama-3")).toEqual({
|
|
53
|
+
reasoning: false,
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should return undefined when no pattern matches", () => {
|
|
58
|
+
const server = makeServer({ qwen: { reasoning: false } });
|
|
59
|
+
expect(server.findOverrideForModel("llama-3")).toBeUndefined();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("should prefer the longest (most specific) matching pattern", () => {
|
|
63
|
+
const server = makeServer({
|
|
64
|
+
llama: { reasoning: false },
|
|
65
|
+
"llama-3-8b": { reasoning: true, contextSize: 128 },
|
|
66
|
+
});
|
|
67
|
+
expect(server.findOverrideForModel("llama-3-8b")).toEqual({
|
|
68
|
+
reasoning: true,
|
|
69
|
+
contextSize: 128,
|
|
70
|
+
});
|
|
71
|
+
expect(server.findOverrideForModel("llama-3-70b")).toEqual({
|
|
72
|
+
reasoning: false,
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("should ignore empty patterns", () => {
|
|
77
|
+
const server = makeServer({ "": { reasoning: false } });
|
|
78
|
+
expect(server.findOverrideForModel("llama-3")).toBeUndefined();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// ─── reasoning override ────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
describe("BaseModel reasoning override", () => {
|
|
85
|
+
it("should default to true when no override matches", () => {
|
|
86
|
+
expect(createModel().reasoning).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("should let the override win over the default", () => {
|
|
90
|
+
const model = createModel({ test: { reasoning: false } });
|
|
91
|
+
expect(model.reasoning).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// ─── capabilities override ────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
describe("BaseModel capabilities override", () => {
|
|
98
|
+
it("should fully replace detection without contacting the server", async () => {
|
|
99
|
+
mockRpc.mockImplementation(() => {
|
|
100
|
+
throw new Error("override must not trigger a fetch");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const model = createModel({ test: { capabilities: ["text", "image"] } });
|
|
104
|
+
const capabilities = await model.getCapabilities();
|
|
105
|
+
|
|
106
|
+
expect(capabilities).toEqual(["text", "image"]);
|
|
107
|
+
expect(mockRpc).not.toHaveBeenCalled();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("should not intercept detection when no override matches", async () => {
|
|
111
|
+
mockDetection(8192);
|
|
112
|
+
|
|
113
|
+
const model = createModel({ other: { capabilities: ["text"] } });
|
|
114
|
+
const capabilities = await model.getCapabilities();
|
|
115
|
+
|
|
116
|
+
expect(capabilities).toEqual(["text"]);
|
|
117
|
+
expect(mockRpc).toHaveBeenCalled();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// ─── contextSize override ─────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
describe("BaseModel.getContextSize with contextSize override", () => {
|
|
124
|
+
it("should return the overridden context size instead of the detected one", async () => {
|
|
125
|
+
mockDetection(8192);
|
|
126
|
+
|
|
127
|
+
const model = createModel({ test: { contextSize: 32768 } });
|
|
128
|
+
const ctxSize = await model.getContextSize();
|
|
129
|
+
|
|
130
|
+
expect(ctxSize).toBe(32768);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("should treat a stored 0 as absent and autodetect", async () => {
|
|
134
|
+
mockDetection(8192);
|
|
135
|
+
|
|
136
|
+
const model = createModel({ test: { contextSize: 0 } });
|
|
137
|
+
const ctxSize = await model.getContextSize();
|
|
138
|
+
|
|
139
|
+
expect(ctxSize).toBe(8192);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("should autodetect when no override matches", async () => {
|
|
143
|
+
mockDetection(8192);
|
|
144
|
+
|
|
145
|
+
const model = createModel({ other: { contextSize: 32768 } });
|
|
146
|
+
const ctxSize = await model.getContextSize();
|
|
147
|
+
|
|
148
|
+
expect(ctxSize).toBe(8192);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// ─── toProviderConfig (cost, maxTokens, compat, precedence) ───────────────
|
|
153
|
+
|
|
154
|
+
describe("toProviderConfig overrides", () => {
|
|
155
|
+
it("should merge a partial cost override with zero defaults", async () => {
|
|
156
|
+
mockDetection(8192);
|
|
157
|
+
|
|
158
|
+
const model = createModel({ test: { cost: { input: 0.2 } } });
|
|
159
|
+
const config = await model.toProviderConfig();
|
|
160
|
+
|
|
161
|
+
expect(config.cost).toEqual({
|
|
162
|
+
input: 0.2,
|
|
163
|
+
output: 0,
|
|
164
|
+
cacheRead: 0,
|
|
165
|
+
cacheWrite: 0,
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("should default every cost field to zero without an override", async () => {
|
|
170
|
+
mockDetection(8192);
|
|
171
|
+
|
|
172
|
+
const model = createModel();
|
|
173
|
+
const config = await model.toProviderConfig();
|
|
174
|
+
|
|
175
|
+
expect(config.cost).toEqual({
|
|
176
|
+
input: 0,
|
|
177
|
+
output: 0,
|
|
178
|
+
cacheRead: 0,
|
|
179
|
+
cacheWrite: 0,
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("should pass a compat override through to the provider config", async () => {
|
|
184
|
+
mockDetection(8192);
|
|
185
|
+
|
|
186
|
+
const compat = {
|
|
187
|
+
supportsStore: false,
|
|
188
|
+
maxTokensField: "max_tokens",
|
|
189
|
+
} as const;
|
|
190
|
+
const model = createModel({ test: { compat } });
|
|
191
|
+
const config = await model.toProviderConfig();
|
|
192
|
+
|
|
193
|
+
expect(config.compat).toEqual(compat);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("should leave compat undefined without an override", async () => {
|
|
197
|
+
mockDetection(8192);
|
|
198
|
+
|
|
199
|
+
const model = createModel();
|
|
200
|
+
const config = await model.toProviderConfig();
|
|
201
|
+
|
|
202
|
+
expect(config.compat).toBeUndefined();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("should use the overridden context size for contextWindow and as the maxTokens fallback", async () => {
|
|
206
|
+
mockDetection(8192);
|
|
207
|
+
|
|
208
|
+
const model = createModel({ test: { contextSize: 32768 } });
|
|
209
|
+
const config = await model.toProviderConfig();
|
|
210
|
+
|
|
211
|
+
expect(config.contextWindow).toBe(32768);
|
|
212
|
+
expect(config.maxTokens).toBe(32768);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("should prefer an explicit maxTokens override over the contextSize override", async () => {
|
|
216
|
+
mockDetection(8192);
|
|
217
|
+
|
|
218
|
+
const model = createModel({
|
|
219
|
+
test: { contextSize: 32768, maxTokens: 4096 },
|
|
220
|
+
});
|
|
221
|
+
const config = await model.toProviderConfig();
|
|
222
|
+
|
|
223
|
+
expect(config.contextWindow).toBe(32768);
|
|
224
|
+
expect(config.maxTokens).toBe(4096);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("should use the detected context size when no override matches", async () => {
|
|
228
|
+
mockDetection(8192);
|
|
229
|
+
|
|
230
|
+
const model = createModel();
|
|
231
|
+
const config = await model.toProviderConfig();
|
|
232
|
+
|
|
233
|
+
expect(config.contextWindow).toBe(8192);
|
|
234
|
+
expect(config.maxTokens).toBe(8192);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("should fall back to FALLBACK_CTX when the model is missing from detection", async () => {
|
|
238
|
+
mockRpc.mockImplementation((endpoint: string) => {
|
|
239
|
+
if (endpoint.startsWith("/props")) {
|
|
240
|
+
return Promise.resolve({ modalities: { vision: false } });
|
|
241
|
+
}
|
|
242
|
+
// /v1/models: no entry for this model → getContextSize throws → fallback
|
|
243
|
+
return Promise.resolve({ data: [] });
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const model = createModel();
|
|
247
|
+
const config = await model.toProviderConfig();
|
|
248
|
+
|
|
249
|
+
expect(config.contextWindow).toBe(FALLBACK_CTX);
|
|
250
|
+
expect(config.maxTokens).toBe(FALLBACK_CTX);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// ─── formatOverrideSummary ────────────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
describe("formatOverrideSummary", () => {
|
|
257
|
+
it("should render every configured field", () => {
|
|
258
|
+
const summary = formatOverrideSummary({
|
|
259
|
+
cost: { input: 0.2, output: 0.6, cacheRead: 0.01 },
|
|
260
|
+
capabilities: ["text", "image"],
|
|
261
|
+
reasoning: false,
|
|
262
|
+
contextSize: 32768,
|
|
263
|
+
maxTokens: 4096,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
expect(summary).toContain("input: $0.2");
|
|
267
|
+
expect(summary).toContain("output: $0.6");
|
|
268
|
+
expect(summary).toContain("cache read: $0.01");
|
|
269
|
+
expect(summary).toContain("capabilities: text,image");
|
|
270
|
+
expect(summary).toContain("reasoning: false");
|
|
271
|
+
expect(summary).toContain("contextSize: 32768");
|
|
272
|
+
expect(summary).toContain("maxTokens: 4096");
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("should omit zero, undefined and empty fields", () => {
|
|
276
|
+
const summary = formatOverrideSummary({
|
|
277
|
+
cost: { input: 0, output: 0.6 },
|
|
278
|
+
reasoning: undefined,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
expect(summary).not.toContain("input:");
|
|
282
|
+
expect(summary).toContain("output: $0.6");
|
|
283
|
+
expect(summary).not.toContain("capabilities");
|
|
284
|
+
expect(summary).not.toContain("reasoning");
|
|
285
|
+
expect(summary).not.toContain("contextSize");
|
|
286
|
+
expect(summary).not.toContain("maxTokens");
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("should render an em dash for an empty override", () => {
|
|
290
|
+
expect(formatOverrideSummary({})).toBe("—");
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// ─── override entry list helpers (overrideEntryEditor) ────────────────────
|
|
295
|
+
|
|
296
|
+
describe("override entry list helpers", () => {
|
|
297
|
+
const servers = [
|
|
298
|
+
{ url: "http://a", overrides: { "a-1": { reasoning: false } } },
|
|
299
|
+
{ url: "http://b" },
|
|
300
|
+
] as LlamaServer[];
|
|
301
|
+
|
|
302
|
+
it("addOverrideEntry should append a pattern → override entry", () => {
|
|
303
|
+
const next = addOverrideEntry(servers, 0, "a-2", { contextSize: 4096 });
|
|
304
|
+
expect(next[0].overrides).toEqual({
|
|
305
|
+
"a-1": { reasoning: false },
|
|
306
|
+
"a-2": { contextSize: 4096 },
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("updateOverrideEntry should replace in place without reordering", () => {
|
|
311
|
+
const next = updateOverrideEntry(servers, 0, 0, "a-renamed", {
|
|
312
|
+
reasoning: true,
|
|
313
|
+
});
|
|
314
|
+
expect(Object.keys(next[0].overrides!)).toEqual(["a-renamed"]);
|
|
315
|
+
expect(next[0].overrides!["a-renamed"]).toEqual({ reasoning: true });
|
|
316
|
+
expect(next[1]).toBe(servers[1]);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("removeOverrideEntry should drop only the targeted entry", () => {
|
|
320
|
+
const withTwo = addOverrideEntry(servers, 0, "a-2", {});
|
|
321
|
+
const next = removeOverrideEntry(withTwo, 0, 0);
|
|
322
|
+
expect(Object.keys(next[0].overrides!)).toEqual(["a-2"]);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("parseCostValue should accept blank, non-negative numbers; reject the rest", () => {
|
|
326
|
+
expect(parseCostValue("")).toBe(0);
|
|
327
|
+
expect(parseCostValue(" 0.5 ")).toBe(0.5);
|
|
328
|
+
expect(parseCostValue("-1")).toBeNull();
|
|
329
|
+
expect(parseCostValue("abc")).toBeNull();
|
|
330
|
+
expect(parseCostValue("Infinity")).toBeNull();
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("applyCostFieldValue should set a positive value, keeping other fields", () => {
|
|
334
|
+
const next = applyCostFieldValue({ input: 0.2 }, "output", 0.6);
|
|
335
|
+
expect(next).toEqual({ input: 0.2, output: 0.6 });
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("applyCostFieldValue should remove the field on zero", () => {
|
|
339
|
+
const next = applyCostFieldValue({ input: 0.2, output: 0.6 }, "input", 0);
|
|
340
|
+
expect(next).toEqual({ output: 0.6 });
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("applyCostFieldValue should return undefined when the last field is zeroed", () => {
|
|
344
|
+
expect(applyCostFieldValue({ input: 0.2 }, "input", 0)).toBeUndefined();
|
|
345
|
+
expect(applyCostFieldValue(undefined, "input", 0)).toBeUndefined();
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("applyCostFieldValue should treat an empty override as unset cost", () => {
|
|
349
|
+
expect(applyCostFieldValue(undefined, "input", 0.5)).toEqual({
|
|
350
|
+
input: 0.5,
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
describe("overrideFieldValue", () => {
|
|
356
|
+
it("formats cost fields, defaulting to 0", () => {
|
|
357
|
+
expect(overrideFieldValue("cost.input", {})).toBe("0");
|
|
358
|
+
expect(overrideFieldValue("cost.input", { cost: { input: 0.2 } })).toBe(
|
|
359
|
+
"0.2",
|
|
360
|
+
);
|
|
361
|
+
expect(overrideFieldValue("cost.cacheWrite", {})).toBe("0");
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("formats capabilities and reasoning labels", () => {
|
|
365
|
+
expect(overrideFieldValue("capabilities", {})).toBe("text");
|
|
366
|
+
expect(
|
|
367
|
+
overrideFieldValue("capabilities", { capabilities: ["text", "image"] }),
|
|
368
|
+
).toBe("text | image");
|
|
369
|
+
expect(overrideFieldValue("reasoning", {})).toBe("true");
|
|
370
|
+
expect(overrideFieldValue("reasoning", { reasoning: false })).toBe("false");
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("formats maxTokens/contextSize, defaulting to 0", () => {
|
|
374
|
+
expect(overrideFieldValue("maxTokens", {})).toBe("0");
|
|
375
|
+
expect(overrideFieldValue("maxTokens", { maxTokens: 4096 })).toBe("4096");
|
|
376
|
+
expect(overrideFieldValue("contextSize", { contextSize: 8192 })).toBe(
|
|
377
|
+
"8192",
|
|
378
|
+
);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it("returns an empty label for unknown fields", () => {
|
|
382
|
+
expect(overrideFieldValue("other", {})).toBe("");
|
|
383
|
+
});
|
|
384
|
+
});
|
package/tests/server.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
import { POLLING_TIMEOUT, SERVER_TIMEOUT } from "../src/constants";
|
|
3
3
|
import { ServerStatus } from "../src/enums/serverStatus";
|
|
4
4
|
import type { LlamaSettingsManager } from "../src/managers/settings";
|
|
@@ -35,6 +35,26 @@ describe("Server providerName", () => {
|
|
|
35
35
|
});
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
+
describe("Server apiBaseUrl", () => {
|
|
39
|
+
it("should append the ENDPOINT_PREFIX to baseUrl", () => {
|
|
40
|
+
const server = new Server(settings, { baseUrl: "http://127.0.0.1:8080" });
|
|
41
|
+
expect(server.apiBaseUrl).toBe("http://127.0.0.1:8080/v1");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should not double the prefix when baseUrl already ends with it", () => {
|
|
45
|
+
const server = new Server(settings, {
|
|
46
|
+
baseUrl: "http://127.0.0.1:8080/v1",
|
|
47
|
+
});
|
|
48
|
+
expect(server.apiBaseUrl).toBe("http://127.0.0.1:8080/v1");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("should keep baseUrl untouched for provider identity", () => {
|
|
52
|
+
const server = new Server(settings, { baseUrl: "http://127.0.0.1:8080" });
|
|
53
|
+
expect(server.baseUrl).toBe("http://127.0.0.1:8080");
|
|
54
|
+
expect(server.providerId).toBe("llama-server=http://127.0.0.1:8080");
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
38
58
|
describe("Server fetchModels", () => {
|
|
39
59
|
it("should call the /models endpoint", async () => {
|
|
40
60
|
mockRpc.mockResolvedValueOnce({
|
|
@@ -154,16 +174,16 @@ describe("Server postRequest", () => {
|
|
|
154
174
|
});
|
|
155
175
|
});
|
|
156
176
|
|
|
157
|
-
describe("Server timeouts", () => {
|
|
158
|
-
it("should resolve timeouts from the injected settings", () => {
|
|
177
|
+
describe("Server timeouts", async () => {
|
|
178
|
+
it("should resolve timeouts from the injected settings", async () => {
|
|
159
179
|
const server = new Server(settings, { baseUrl: "http://127.0.0.1:8080" });
|
|
160
180
|
|
|
161
|
-
expect(server.
|
|
162
|
-
expect(server.
|
|
181
|
+
expect(await server.getServerTimeout()).toBe(SERVER_TIMEOUT);
|
|
182
|
+
expect(await server.getPollingTimeout()).toBe(POLLING_TIMEOUT);
|
|
163
183
|
});
|
|
164
184
|
|
|
165
|
-
it("should return resolved custom values", () => {
|
|
166
|
-
vi.mocked(settings.resolveTimeouts).
|
|
185
|
+
it("should return resolved custom values", async () => {
|
|
186
|
+
vi.mocked(settings.resolveTimeouts).mockResolvedValue({
|
|
167
187
|
pollingTimeout: 90000,
|
|
168
188
|
serverTimeout: 2000,
|
|
169
189
|
});
|
|
@@ -174,29 +194,39 @@ describe("Server timeouts", () => {
|
|
|
174
194
|
customName: "My Server",
|
|
175
195
|
});
|
|
176
196
|
|
|
177
|
-
expect(server.
|
|
197
|
+
expect(await server.getPollingTimeout()).toBe(90000);
|
|
178
198
|
});
|
|
179
199
|
|
|
180
|
-
it("should read timeouts live from settings", () => {
|
|
200
|
+
it("should read timeouts live from settings", async () => {
|
|
181
201
|
const server = new Server(settings, { baseUrl: "http://127.0.0.1:8080" });
|
|
182
202
|
|
|
183
|
-
vi.mocked(settings.resolveTimeouts).
|
|
203
|
+
vi.mocked(settings.resolveTimeouts).mockResolvedValue({
|
|
184
204
|
pollingTimeout: 120000,
|
|
185
205
|
serverTimeout: 3000,
|
|
186
206
|
});
|
|
187
|
-
expect(server.
|
|
207
|
+
expect(await server.getPollingTimeout()).toBe(120000);
|
|
188
208
|
|
|
189
|
-
vi.mocked(settings.resolveTimeouts).
|
|
209
|
+
vi.mocked(settings.resolveTimeouts).mockResolvedValue({
|
|
190
210
|
pollingTimeout: 90000,
|
|
191
211
|
serverTimeout: 2000,
|
|
192
212
|
});
|
|
193
|
-
expect(server.
|
|
213
|
+
expect(await server.getPollingTimeout()).toBe(90000);
|
|
194
214
|
});
|
|
195
215
|
});
|
|
196
216
|
|
|
197
217
|
describe("Server isReady", () => {
|
|
218
|
+
// `isReady` delegates to the shared health probe (`utils/health`), which
|
|
219
|
+
// uses plain `fetch` instead of the ApiClient — stub it per test.
|
|
220
|
+
const stubFetch = (impl: () => Promise<unknown>) => {
|
|
221
|
+
vi.stubGlobal("fetch", vi.fn(impl));
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
afterEach(() => {
|
|
225
|
+
vi.unstubAllGlobals();
|
|
226
|
+
});
|
|
227
|
+
|
|
198
228
|
it("should return READY when health status is ok", async () => {
|
|
199
|
-
|
|
229
|
+
stubFetch(async () => ({ json: async () => ({ status: "ok" }) }));
|
|
200
230
|
|
|
201
231
|
const server = createMockServer();
|
|
202
232
|
const status = await server.isReady(1000);
|
|
@@ -205,7 +235,9 @@ describe("Server isReady", () => {
|
|
|
205
235
|
});
|
|
206
236
|
|
|
207
237
|
it("should return UNREACHABLE when health check fails", async () => {
|
|
208
|
-
|
|
238
|
+
stubFetch(async () => {
|
|
239
|
+
throw new Error("connection refused");
|
|
240
|
+
});
|
|
209
241
|
|
|
210
242
|
const server = createMockServer();
|
|
211
243
|
const status = await server.isReady(1000);
|
|
@@ -214,11 +246,22 @@ describe("Server isReady", () => {
|
|
|
214
246
|
});
|
|
215
247
|
|
|
216
248
|
it("should return UNREACHABLE when health status is not ok", async () => {
|
|
217
|
-
|
|
249
|
+
stubFetch(async () => ({ json: async () => ({ status: "error" }) }));
|
|
218
250
|
|
|
219
251
|
const server = createMockServer();
|
|
220
252
|
const status = await server.isReady(1000);
|
|
221
253
|
|
|
222
254
|
expect(status).toBe(ServerStatus.UNREACHABLE);
|
|
223
255
|
});
|
|
256
|
+
|
|
257
|
+
it("should return TIMEOUT when the health check aborts", async () => {
|
|
258
|
+
stubFetch(async () => {
|
|
259
|
+
throw new DOMException("The operation timed out.", "TimeoutError");
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const server = createMockServer();
|
|
263
|
+
const status = await server.isReady(1000);
|
|
264
|
+
|
|
265
|
+
expect(status).toBe(ServerStatus.TIMEOUT);
|
|
266
|
+
});
|
|
224
267
|
});
|