pi-llama-cpp 0.9.1 → 0.10.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 +97 -22
- package/package.json +7 -6
- package/src/constants.ts +13 -3
- package/src/index.ts +4 -10
- package/src/interfaces/settings.ts +60 -0
- package/src/managers/events.ts +33 -9
- package/src/managers/server.ts +6 -4
- package/src/managers/settings.ts +206 -0
- package/src/models/baseModel.ts +32 -9
- package/src/models/legacyModel.ts +2 -2
- package/src/models/routerModel.ts +3 -3
- package/src/server.ts +33 -9
- package/src/sse/manager.ts +2 -1
- package/tests/events.test.ts +186 -11
- package/tests/mocks.ts +1 -1
- package/tests/server.test.ts +46 -0
- package/tests/settings.test.ts +636 -0
- package/tests/sseManager.test.ts +21 -0
- package/src/resolver.ts +0 -149
- package/tests/resolver.test.ts +0 -184
package/src/models/baseModel.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ProviderModelConfig } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
2
|
+
import { FALLBACK_CTX, POLLING_INTERVAL } from "../constants";
|
|
3
3
|
import { Mode } from "../enums/mode";
|
|
4
4
|
import { Status } from "../enums/status";
|
|
5
5
|
import { DataProperty } from "../interfaces/endpoints/models";
|
|
@@ -130,9 +130,9 @@ export abstract class BaseModel {
|
|
|
130
130
|
const { data } = await this.server.fetchModels();
|
|
131
131
|
const { n_ctx } = data.find((m) => m.id === this.id)?.meta!;
|
|
132
132
|
|
|
133
|
-
return n_ctx ??
|
|
133
|
+
return n_ctx ?? FALLBACK_CTX;
|
|
134
134
|
} catch {
|
|
135
|
-
return
|
|
135
|
+
return FALLBACK_CTX;
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
|
|
@@ -202,11 +202,31 @@ export abstract class BaseModel {
|
|
|
202
202
|
await this.server.postRequest("load", this.id);
|
|
203
203
|
|
|
204
204
|
if (await this.server.sseManager.probeSSE()) {
|
|
205
|
-
|
|
206
|
-
|
|
205
|
+
try {
|
|
206
|
+
const { status, exit_code } =
|
|
207
|
+
await this.server.sseManager.subscribeToStatus(this.id);
|
|
207
208
|
|
|
208
|
-
|
|
209
|
-
|
|
209
|
+
if (status === "failed" || (status === "unloaded" && exit_code !== 0)) {
|
|
210
|
+
throw new Error(`Model loading failed: ${this.id}`);
|
|
211
|
+
}
|
|
212
|
+
} catch (err) {
|
|
213
|
+
// A real failed status from the server stays fatal.
|
|
214
|
+
if (
|
|
215
|
+
err instanceof Error &&
|
|
216
|
+
err.message.startsWith("Model loading failed")
|
|
217
|
+
) {
|
|
218
|
+
throw err;
|
|
219
|
+
}
|
|
220
|
+
// An SSE timeout or connection failure is not proof the load failed:
|
|
221
|
+
// subscribeToStatus timers are only cleared by a terminal event on
|
|
222
|
+
// the same SSE connection, so a timer orphaned by a server restart
|
|
223
|
+
// or reconnect can fire during a later, healthy load. Check the
|
|
224
|
+
// model's real status over HTTP before surfacing an error.
|
|
225
|
+
await this.pollStatus();
|
|
226
|
+
const finalStatus = await this.getStatus();
|
|
227
|
+
if (finalStatus !== Status.LOADED && finalStatus !== Status.SLEEPING) {
|
|
228
|
+
throw new Error(`Model loading failed: ${this.id}`);
|
|
229
|
+
}
|
|
210
230
|
}
|
|
211
231
|
} else {
|
|
212
232
|
await this.pollStatus();
|
|
@@ -224,14 +244,17 @@ export abstract class BaseModel {
|
|
|
224
244
|
* Polls llama-server to check when the model is loaded
|
|
225
245
|
*
|
|
226
246
|
* @param startTime The initial polling timestamp
|
|
227
|
-
* @param timeout The maximum amount of ms before timeout. Defaults to
|
|
247
|
+
* @param timeout The maximum amount of ms before timeout. Defaults to server's pollingTimeout
|
|
228
248
|
* @param interval The polling interval. Defaults to POLLING_INTERVAL
|
|
229
249
|
*/
|
|
230
250
|
async pollStatus(
|
|
231
251
|
startTime: number = Date.now(),
|
|
232
|
-
timeout
|
|
252
|
+
timeout?: number,
|
|
233
253
|
interval: number = POLLING_INTERVAL,
|
|
234
254
|
): Promise<void> {
|
|
255
|
+
if (timeout === undefined) {
|
|
256
|
+
timeout = this.server.pollingTimeout;
|
|
257
|
+
}
|
|
235
258
|
while ((await this.getStatus()) === Status.LOADING) {
|
|
236
259
|
// Force a timeout if we wasted too much time polling
|
|
237
260
|
if (Date.now() - startTime > timeout) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { FALLBACK_CTX } from "../constants";
|
|
2
2
|
import { Mode } from "../enums/mode";
|
|
3
3
|
import { SingleModel } from "./singleModel";
|
|
4
4
|
|
|
@@ -25,7 +25,7 @@ export class LegacyModel extends SingleModel {
|
|
|
25
25
|
const [{ max_model_len }] = data;
|
|
26
26
|
const contextSize = max_model_len === 0 ? n_ctx : max_model_len;
|
|
27
27
|
|
|
28
|
-
return contextSize ??
|
|
28
|
+
return contextSize ?? FALLBACK_CTX;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { FALLBACK_CTX, POLLING_INTERVAL } from "../constants";
|
|
2
2
|
import { Mode } from "../enums/mode";
|
|
3
3
|
import { Status } from "../enums/status";
|
|
4
4
|
import { BaseModel } from "./baseModel";
|
|
@@ -41,7 +41,7 @@ export class RouterModel extends BaseModel {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
const timeout =
|
|
44
|
+
const timeout = this.server.pollingTimeout - elapsed;
|
|
45
45
|
return await super.pollStatus(startTime, timeout);
|
|
46
46
|
}
|
|
47
47
|
|
|
@@ -60,7 +60,7 @@ export class RouterModel extends BaseModel {
|
|
|
60
60
|
const response =
|
|
61
61
|
this.extractFrom("--ctx-size") ??
|
|
62
62
|
this.extractFrom("--fit-ctx") ??
|
|
63
|
-
|
|
63
|
+
FALLBACK_CTX;
|
|
64
64
|
|
|
65
65
|
return response;
|
|
66
66
|
}
|
package/src/server.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { ApiClient } from "./api/client";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
API_KEY_PLACEHOLDER,
|
|
4
|
+
POLLING_TIMEOUT,
|
|
5
|
+
PROVIDER_NAME,
|
|
6
|
+
PROVIDER_PREFIX,
|
|
7
|
+
SERVER_TIMEOUT,
|
|
8
|
+
} from "./constants";
|
|
3
9
|
import { Mode } from "./enums/mode";
|
|
4
10
|
import { ServerStatus } from "./enums/serverStatus";
|
|
5
11
|
import { HealthEndpoint } from "./interfaces/endpoints/health";
|
|
@@ -8,20 +14,25 @@ import {
|
|
|
8
14
|
PropsEndpoint,
|
|
9
15
|
PropsModelEndpoint,
|
|
10
16
|
} from "./interfaces/endpoints/props";
|
|
17
|
+
import { settings } from "./managers/settings";
|
|
11
18
|
import { BaseModel } from "./models/baseModel";
|
|
12
19
|
import { LegacyModel } from "./models/legacyModel";
|
|
13
20
|
import { RouterModel } from "./models/routerModel";
|
|
14
21
|
import { SingleModel } from "./models/singleModel";
|
|
15
|
-
import { ConfigResolver } from "./resolver";
|
|
16
22
|
import { SSEManager } from "./sse/manager";
|
|
17
23
|
|
|
18
24
|
export class Server {
|
|
19
25
|
public readonly models: BaseModel[] = [];
|
|
20
|
-
private configResolver = new ConfigResolver();
|
|
21
26
|
private apiClient!: ApiClient;
|
|
22
27
|
private sse!: SSEManager;
|
|
23
28
|
|
|
24
|
-
constructor(
|
|
29
|
+
constructor(
|
|
30
|
+
readonly baseUrl: string,
|
|
31
|
+
private readonly customId?: string,
|
|
32
|
+
private readonly customName?: string,
|
|
33
|
+
readonly serverTimeout: number = SERVER_TIMEOUT,
|
|
34
|
+
readonly pollingTimeout: number = POLLING_TIMEOUT,
|
|
35
|
+
) {}
|
|
25
36
|
|
|
26
37
|
/**
|
|
27
38
|
* Provides access to the SSE manager for direct subscriptions.
|
|
@@ -32,24 +43,37 @@ export class Server {
|
|
|
32
43
|
|
|
33
44
|
/**
|
|
34
45
|
* Generates a unique provider ID from a server URL.
|
|
46
|
+
* Uses custom ID if provided, otherwise falls back to URL-based ID.
|
|
35
47
|
*/
|
|
36
48
|
get providerId(): string {
|
|
37
|
-
return `${PROVIDER_PREFIX}=${this.baseUrl}`;
|
|
49
|
+
return this.customId ?? `${PROVIDER_PREFIX}=${this.baseUrl}`;
|
|
38
50
|
}
|
|
39
51
|
|
|
40
52
|
/**
|
|
41
53
|
* Generates a human-readable provider name from a server URL.
|
|
54
|
+
* Uses custom name as suffix if provided.
|
|
42
55
|
*/
|
|
43
56
|
get providerName(): string {
|
|
57
|
+
if (this.customName) {
|
|
58
|
+
return `${PROVIDER_NAME} (${this.customName})`;
|
|
59
|
+
}
|
|
44
60
|
return `${PROVIDER_NAME} (${this.baseUrl})`;
|
|
45
61
|
}
|
|
46
62
|
|
|
47
63
|
/**
|
|
48
|
-
* Retrieves the API key from the resolver
|
|
64
|
+
* Retrieves the API key from the config resolver.
|
|
65
|
+
* Tries custom ID first, then falls back to URL-based ID.
|
|
66
|
+
*
|
|
49
67
|
* @returns The API key
|
|
50
68
|
*/
|
|
51
|
-
|
|
52
|
-
|
|
69
|
+
getApiKey(): string {
|
|
70
|
+
// Try custom ID first
|
|
71
|
+
if (this.customId) {
|
|
72
|
+
const key = settings.resolveApiKey(this.customId);
|
|
73
|
+
if (key !== API_KEY_PLACEHOLDER) return key;
|
|
74
|
+
}
|
|
75
|
+
// Fall back to URL-based ID
|
|
76
|
+
return settings.resolveApiKey(`${PROVIDER_PREFIX}=${this.baseUrl}`);
|
|
53
77
|
}
|
|
54
78
|
|
|
55
79
|
/**
|
|
@@ -59,7 +83,7 @@ export class Server {
|
|
|
59
83
|
async initialize() {
|
|
60
84
|
const apiKey = await this.getApiKey();
|
|
61
85
|
this.apiClient = new ApiClient(this.baseUrl, apiKey);
|
|
62
|
-
this.sse = new SSEManager(this.baseUrl, apiKey);
|
|
86
|
+
this.sse = new SSEManager(this.baseUrl, apiKey, this.serverTimeout);
|
|
63
87
|
const { data } = await this.fetchModels();
|
|
64
88
|
const mode = await this.detectServerMode();
|
|
65
89
|
|
package/src/sse/manager.ts
CHANGED
|
@@ -27,6 +27,7 @@ export class SSEManager {
|
|
|
27
27
|
constructor(
|
|
28
28
|
private readonly baseUrl: string,
|
|
29
29
|
private readonly apiKey: string,
|
|
30
|
+
readonly serverTimeout: number = SERVER_TIMEOUT,
|
|
30
31
|
) {}
|
|
31
32
|
|
|
32
33
|
/**
|
|
@@ -52,7 +53,7 @@ export class SSEManager {
|
|
|
52
53
|
}
|
|
53
54
|
const response = await fetch(url, {
|
|
54
55
|
method: "GET",
|
|
55
|
-
signal: AbortSignal.timeout(
|
|
56
|
+
signal: AbortSignal.timeout(this.serverTimeout),
|
|
56
57
|
});
|
|
57
58
|
this.sseSupported =
|
|
58
59
|
response.ok &&
|
package/tests/events.test.ts
CHANGED
|
@@ -1,7 +1,25 @@
|
|
|
1
1
|
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import { THINKING_BUDGETS } from "../src/constants";
|
|
3
|
+
import { Status } from "../src/enums/status";
|
|
3
4
|
import { createMockModel, createMockServer } from "./mocks";
|
|
4
5
|
|
|
6
|
+
// Mock settings — reactToModelSelect and autoloadOnMessage
|
|
7
|
+
const mockSettings = {
|
|
8
|
+
resolveReactToModelSelect: vi.fn(() => true),
|
|
9
|
+
resolveAutoloadOnMessage: vi.fn(() => false),
|
|
10
|
+
resolveThinkingLevel: vi.fn(() => "medium"),
|
|
11
|
+
resolveThinkingBudgets: vi.fn(() => ({ ...THINKING_BUDGETS })),
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// Wire resolveThinkingBudgets to use the SettingsManager mock when set
|
|
15
|
+
mockSettings.resolveThinkingBudgets.mockImplementation(() => {
|
|
16
|
+
const userBudgets = mockSettingsManager.getThinkingBudgets();
|
|
17
|
+
if (userBudgets) {
|
|
18
|
+
return { ...THINKING_BUDGETS, ...userBudgets };
|
|
19
|
+
}
|
|
20
|
+
return { ...THINKING_BUDGETS };
|
|
21
|
+
});
|
|
22
|
+
|
|
5
23
|
// Create a mutable mock object shared across tests
|
|
6
24
|
const mockSettingsManager = {
|
|
7
25
|
getDefaultThinkingLevel: vi.fn(() => "medium"),
|
|
@@ -19,6 +37,10 @@ vi.mock("@earendil-works/pi-coding-agent", async (importOriginal) => {
|
|
|
19
37
|
};
|
|
20
38
|
});
|
|
21
39
|
|
|
40
|
+
vi.mock("../src/managers/settings", () => ({
|
|
41
|
+
settings: mockSettings,
|
|
42
|
+
}));
|
|
43
|
+
|
|
22
44
|
let EventManager: typeof import("../src/managers/events").EventManager;
|
|
23
45
|
|
|
24
46
|
beforeAll(async () => {
|
|
@@ -44,6 +66,21 @@ const createNonLlamaPayload = () => ({
|
|
|
44
66
|
messages: [{ role: "user", content: "hello" }],
|
|
45
67
|
});
|
|
46
68
|
|
|
69
|
+
const createMockCtx = (
|
|
70
|
+
thinkingLevel?:
|
|
71
|
+
| "off"
|
|
72
|
+
| "minimal"
|
|
73
|
+
| "low"
|
|
74
|
+
| "medium"
|
|
75
|
+
| "high"
|
|
76
|
+
| "xhigh"
|
|
77
|
+
| "max",
|
|
78
|
+
) =>
|
|
79
|
+
({
|
|
80
|
+
thinkingLevel,
|
|
81
|
+
ui: { notify: vi.fn() },
|
|
82
|
+
}) as any;
|
|
83
|
+
|
|
47
84
|
describe("EventManager.onBeforeProviderRequest", () => {
|
|
48
85
|
describe("normal usage — each thinking level", () => {
|
|
49
86
|
it.each([
|
|
@@ -68,8 +105,10 @@ describe("EventManager.onBeforeProviderRequest", () => {
|
|
|
68
105
|
const eventManager = new EventManager([server]);
|
|
69
106
|
const event = { payload: createPayload("model-a") };
|
|
70
107
|
|
|
108
|
+
const ctx = createMockCtx(level as any);
|
|
71
109
|
const result = (await eventManager.onBeforeProviderRequest(
|
|
72
110
|
event as any,
|
|
111
|
+
ctx,
|
|
73
112
|
)) as Record<string, unknown>;
|
|
74
113
|
|
|
75
114
|
expect(result.model).toBe("model-a");
|
|
@@ -92,13 +131,15 @@ describe("EventManager.onBeforeProviderRequest", () => {
|
|
|
92
131
|
},
|
|
93
132
|
};
|
|
94
133
|
|
|
134
|
+
const ctx = createMockCtx("low");
|
|
95
135
|
const result = (await eventManager.onBeforeProviderRequest(
|
|
96
136
|
event as any,
|
|
137
|
+
ctx,
|
|
97
138
|
)) as Record<string, unknown>;
|
|
98
139
|
|
|
99
140
|
expect(result.messages).toEqual([{ role: "user", content: "test" }]);
|
|
100
141
|
expect(result.temperature).toBe(0.7);
|
|
101
|
-
expect(result.thinking_budget_tokens).toBe(
|
|
142
|
+
expect(result.thinking_budget_tokens).toBe(THINKING_BUDGETS.low);
|
|
102
143
|
});
|
|
103
144
|
});
|
|
104
145
|
|
|
@@ -110,7 +151,11 @@ describe("EventManager.onBeforeProviderRequest", () => {
|
|
|
110
151
|
const eventManager = new EventManager([server]);
|
|
111
152
|
const event = { payload: createNonLlamaPayload() };
|
|
112
153
|
|
|
113
|
-
const
|
|
154
|
+
const ctx = createMockCtx();
|
|
155
|
+
const result = await eventManager.onBeforeProviderRequest(
|
|
156
|
+
event as any,
|
|
157
|
+
ctx,
|
|
158
|
+
);
|
|
114
159
|
|
|
115
160
|
expect(result).toEqual(createNonLlamaPayload());
|
|
116
161
|
});
|
|
@@ -124,7 +169,11 @@ describe("EventManager.onBeforeProviderRequest", () => {
|
|
|
124
169
|
const eventManager = new EventManager([server]);
|
|
125
170
|
const event = { payload: { messages: [] } };
|
|
126
171
|
|
|
127
|
-
const
|
|
172
|
+
const ctx = createMockCtx();
|
|
173
|
+
const result = await eventManager.onBeforeProviderRequest(
|
|
174
|
+
event as any,
|
|
175
|
+
ctx,
|
|
176
|
+
);
|
|
128
177
|
|
|
129
178
|
expect(result).toEqual({ messages: [] });
|
|
130
179
|
});
|
|
@@ -141,8 +190,10 @@ describe("EventManager.onBeforeProviderRequest", () => {
|
|
|
141
190
|
const eventManager = new EventManager([server]);
|
|
142
191
|
const event = { payload: createPayload("model-a") };
|
|
143
192
|
|
|
193
|
+
const ctx = createMockCtx("low");
|
|
144
194
|
const result = (await eventManager.onBeforeProviderRequest(
|
|
145
195
|
event as any,
|
|
196
|
+
ctx,
|
|
146
197
|
)) as Record<string, unknown>;
|
|
147
198
|
|
|
148
199
|
expect(result.thinking_budget_tokens).toBe(4096);
|
|
@@ -158,14 +209,14 @@ describe("EventManager.onBeforeProviderRequest", () => {
|
|
|
158
209
|
const eventManager = new EventManager([server]);
|
|
159
210
|
const event = { payload: createPayload("model-a") };
|
|
160
211
|
|
|
212
|
+
const ctx = createMockCtx("medium");
|
|
161
213
|
const result = (await eventManager.onBeforeProviderRequest(
|
|
162
214
|
event as any,
|
|
215
|
+
ctx,
|
|
163
216
|
)) as Record<string, unknown>;
|
|
164
217
|
|
|
165
218
|
// medium uses default since user only overrode low
|
|
166
|
-
expect(result.thinking_budget_tokens).toBe(
|
|
167
|
-
DEFAULT_THINKING_BUDGETS.medium,
|
|
168
|
-
);
|
|
219
|
+
expect(result.thinking_budget_tokens).toBe(THINKING_BUDGETS.medium);
|
|
169
220
|
});
|
|
170
221
|
});
|
|
171
222
|
|
|
@@ -185,14 +236,14 @@ describe("EventManager.onBeforeProviderRequest", () => {
|
|
|
185
236
|
const eventManager = new EventManager([server]);
|
|
186
237
|
const event = { payload: createPayload("model-a") };
|
|
187
238
|
|
|
239
|
+
const ctx = createMockCtx("medium");
|
|
188
240
|
const result = (await eventManager.onBeforeProviderRequest(
|
|
189
241
|
event as any,
|
|
242
|
+
ctx,
|
|
190
243
|
)) as Record<string, unknown>;
|
|
191
244
|
|
|
192
245
|
// Should fall back to default since "medium" is not in user budgets
|
|
193
|
-
expect(result.thinking_budget_tokens).toBe(
|
|
194
|
-
DEFAULT_THINKING_BUDGETS.medium,
|
|
195
|
-
);
|
|
246
|
+
expect(result.thinking_budget_tokens).toBe(THINKING_BUDGETS.medium);
|
|
196
247
|
});
|
|
197
248
|
|
|
198
249
|
it("should not allow overriding 'off' — thinking stays disabled", async () => {
|
|
@@ -207,8 +258,10 @@ describe("EventManager.onBeforeProviderRequest", () => {
|
|
|
207
258
|
const eventManager = new EventManager([server]);
|
|
208
259
|
const event = { payload: createPayload("model-a") };
|
|
209
260
|
|
|
261
|
+
const ctx = createMockCtx("off");
|
|
210
262
|
const result = (await eventManager.onBeforeProviderRequest(
|
|
211
263
|
event as any,
|
|
264
|
+
ctx,
|
|
212
265
|
)) as Record<string, unknown>;
|
|
213
266
|
|
|
214
267
|
expect(result).toMatchObject({
|
|
@@ -229,8 +282,10 @@ describe("EventManager.onBeforeProviderRequest", () => {
|
|
|
229
282
|
const eventManager = new EventManager([server]);
|
|
230
283
|
const event = { payload: createPayload("model-a") };
|
|
231
284
|
|
|
285
|
+
const ctx = createMockCtx("max");
|
|
232
286
|
const result = (await eventManager.onBeforeProviderRequest(
|
|
233
287
|
event as any,
|
|
288
|
+
ctx,
|
|
234
289
|
)) as Record<string, unknown>;
|
|
235
290
|
|
|
236
291
|
expect(result).toEqual(createPayload("model-a"));
|
|
@@ -247,11 +302,131 @@ describe("EventManager.onBeforeProviderRequest", () => {
|
|
|
247
302
|
const eventManager = new EventManager([server]);
|
|
248
303
|
const event = { payload: createPayload("model-a") };
|
|
249
304
|
|
|
305
|
+
const ctx = createMockCtx("high");
|
|
250
306
|
const result = (await eventManager.onBeforeProviderRequest(
|
|
251
307
|
event as any,
|
|
308
|
+
ctx,
|
|
252
309
|
)) as Record<string, unknown>;
|
|
253
310
|
|
|
254
|
-
expect(result.thinking_budget_tokens).toBe(
|
|
311
|
+
expect(result.thinking_budget_tokens).toBe(THINKING_BUDGETS.high);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
describe("EventManager.onModelSelect", () => {
|
|
317
|
+
beforeEach(() => {
|
|
318
|
+
mockSettings.resolveReactToModelSelect.mockReturnValue(true);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("should load the model when reactToModelSelect is true", async () => {
|
|
322
|
+
const server = createMockServer({
|
|
323
|
+
models: ["model-a"].map((id) => createMockModel(id)),
|
|
324
|
+
});
|
|
325
|
+
const eventManager = new EventManager([server]);
|
|
326
|
+
const ctx = createMockCtx();
|
|
327
|
+
|
|
328
|
+
const event = {
|
|
329
|
+
model: { provider: server.providerId, id: "model-a" },
|
|
330
|
+
} as any;
|
|
331
|
+
|
|
332
|
+
await eventManager.onModelSelect(event, ctx);
|
|
333
|
+
|
|
334
|
+
expect(ctx.ui.notify).toHaveBeenCalledWith("Loading model-a...", "info");
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it("should return early when reactToModelSelect is false", async () => {
|
|
338
|
+
mockSettings.resolveReactToModelSelect.mockReturnValue(false);
|
|
339
|
+
|
|
340
|
+
const server = createMockServer({
|
|
341
|
+
models: ["model-a"].map((id) => createMockModel(id)),
|
|
342
|
+
});
|
|
343
|
+
const eventManager = new EventManager([server]);
|
|
344
|
+
const ctx = createMockCtx();
|
|
345
|
+
|
|
346
|
+
const event = {
|
|
347
|
+
model: { provider: server.providerId, id: "model-a" },
|
|
348
|
+
} as any;
|
|
349
|
+
|
|
350
|
+
await eventManager.onModelSelect(event, ctx);
|
|
351
|
+
|
|
352
|
+
expect(ctx.ui.notify).not.toHaveBeenCalled();
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
describe("EventManager.autoLoadIfNeeded", () => {
|
|
357
|
+
it("should load the model when autoloadOnMessage is true and model is UNLOADED", async () => {
|
|
358
|
+
mockSettings.resolveAutoloadOnMessage.mockReturnValue(true);
|
|
359
|
+
|
|
360
|
+
const server = createMockServer({
|
|
361
|
+
models: [
|
|
362
|
+
createMockModel("model-a", {
|
|
363
|
+
getStatus: vi.fn().mockResolvedValue(Status.UNLOADED),
|
|
364
|
+
load: vi.fn().mockResolvedValue(undefined),
|
|
365
|
+
}),
|
|
366
|
+
],
|
|
367
|
+
});
|
|
368
|
+
const eventManager = new EventManager([server]);
|
|
369
|
+
const model = server.models[0];
|
|
370
|
+
|
|
371
|
+
await (eventManager as any).autoLoadIfNeeded(model);
|
|
372
|
+
|
|
373
|
+
expect(model.load).toHaveBeenCalled();
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it("should not load the model when autoloadOnMessage is false", async () => {
|
|
377
|
+
mockSettings.resolveAutoloadOnMessage.mockReturnValue(false);
|
|
378
|
+
|
|
379
|
+
const server = createMockServer({
|
|
380
|
+
models: [
|
|
381
|
+
createMockModel("model-a", {
|
|
382
|
+
getStatus: vi.fn().mockResolvedValue(Status.UNLOADED),
|
|
383
|
+
load: vi.fn().mockResolvedValue(undefined),
|
|
384
|
+
}),
|
|
385
|
+
],
|
|
386
|
+
});
|
|
387
|
+
const eventManager = new EventManager([server]);
|
|
388
|
+
const model = server.models[0];
|
|
389
|
+
|
|
390
|
+
await (eventManager as any).autoLoadIfNeeded(model);
|
|
391
|
+
|
|
392
|
+
expect(model.load).not.toHaveBeenCalled();
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it("should not load the model when model is already LOADED", async () => {
|
|
396
|
+
mockSettings.resolveAutoloadOnMessage.mockReturnValue(true);
|
|
397
|
+
|
|
398
|
+
const server = createMockServer({
|
|
399
|
+
models: [
|
|
400
|
+
createMockModel("model-a", {
|
|
401
|
+
getStatus: vi.fn().mockResolvedValue(Status.LOADED),
|
|
402
|
+
load: vi.fn().mockResolvedValue(undefined),
|
|
403
|
+
}),
|
|
404
|
+
],
|
|
255
405
|
});
|
|
406
|
+
const eventManager = new EventManager([server]);
|
|
407
|
+
const model = server.models[0];
|
|
408
|
+
|
|
409
|
+
await (eventManager as any).autoLoadIfNeeded(model);
|
|
410
|
+
|
|
411
|
+
expect(model.load).not.toHaveBeenCalled();
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it("should not load the model when model is SLEEPING", async () => {
|
|
415
|
+
mockSettings.resolveAutoloadOnMessage.mockReturnValue(true);
|
|
416
|
+
|
|
417
|
+
const server = createMockServer({
|
|
418
|
+
models: [
|
|
419
|
+
createMockModel("model-a", {
|
|
420
|
+
getStatus: vi.fn().mockResolvedValue(Status.SLEEPING),
|
|
421
|
+
load: vi.fn().mockResolvedValue(undefined),
|
|
422
|
+
}),
|
|
423
|
+
],
|
|
424
|
+
});
|
|
425
|
+
const eventManager = new EventManager([server]);
|
|
426
|
+
const model = server.models[0];
|
|
427
|
+
|
|
428
|
+
await (eventManager as any).autoLoadIfNeeded(model);
|
|
429
|
+
|
|
430
|
+
expect(model.load).not.toHaveBeenCalled();
|
|
256
431
|
});
|
|
257
432
|
});
|
package/tests/mocks.ts
CHANGED
|
@@ -17,7 +17,7 @@ export const createMockServer = (
|
|
|
17
17
|
const server: Partial<Server> = {
|
|
18
18
|
baseUrl: "http://127.0.0.1:8080",
|
|
19
19
|
models,
|
|
20
|
-
getApiKey: () =>
|
|
20
|
+
getApiKey: () => overrides.apiKey ?? "",
|
|
21
21
|
fetchModels: () => mockRpc("/v1/models"),
|
|
22
22
|
fetchModelProps: (modelId: string) =>
|
|
23
23
|
mockRpc(`/props?model=${modelId}&autoload=false`),
|
package/tests/server.test.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { POLLING_TIMEOUT, SERVER_TIMEOUT } from "../src/constants";
|
|
2
3
|
import { ServerStatus } from "../src/enums/serverStatus";
|
|
3
4
|
import { Server } from "../src/server";
|
|
4
5
|
import { createMockServer, mockRpc } from "./mocks";
|
|
@@ -146,6 +147,51 @@ describe("Server postRequest", () => {
|
|
|
146
147
|
});
|
|
147
148
|
});
|
|
148
149
|
|
|
150
|
+
describe("Server timeouts", () => {
|
|
151
|
+
it("should use default timeouts when not provided", () => {
|
|
152
|
+
const server = new Server("http://127.0.0.1:8080");
|
|
153
|
+
|
|
154
|
+
expect(server.serverTimeout).toBe(SERVER_TIMEOUT);
|
|
155
|
+
expect(server.pollingTimeout).toBe(POLLING_TIMEOUT);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("should accept custom serverTimeout", () => {
|
|
159
|
+
const server = new Server(
|
|
160
|
+
"http://127.0.0.1:8080",
|
|
161
|
+
undefined,
|
|
162
|
+
undefined,
|
|
163
|
+
3000,
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
expect(server.serverTimeout).toBe(3000);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("should accept custom pollingTimeout", () => {
|
|
170
|
+
const server = new Server(
|
|
171
|
+
"http://127.0.0.1:8080",
|
|
172
|
+
undefined,
|
|
173
|
+
undefined,
|
|
174
|
+
1000,
|
|
175
|
+
120000,
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
expect(server.pollingTimeout).toBe(120000);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("should accept both custom timeouts", () => {
|
|
182
|
+
const server = new Server(
|
|
183
|
+
"http://127.0.0.1:8080",
|
|
184
|
+
"my-id",
|
|
185
|
+
"My Server",
|
|
186
|
+
2000,
|
|
187
|
+
90000,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
expect(server.serverTimeout).toBe(2000);
|
|
191
|
+
expect(server.pollingTimeout).toBe(90000);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
149
195
|
describe("Server isReady", () => {
|
|
150
196
|
it("should return READY when health status is ok", async () => {
|
|
151
197
|
mockRpc.mockResolvedValueOnce({ status: "ok" });
|