pi-llama-cpp 0.5.1 → 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 -133
- 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 -96
- package/src/tools/resolver.ts +0 -136
- package/src/tools/retriever.ts +0 -71
- package/tests/handlers.test.ts +0 -164
- package/tests/modelsCommand.test.ts +0 -270
package/src/server.ts
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { PROVIDER_NAME, PROVIDER_PREFIX } from "./constants";
|
|
2
|
+
import { Mode } from "./enums/mode";
|
|
3
|
+
import { HealthEndpoint } from "./interfaces/endpoints/health";
|
|
4
|
+
import { ModelsEndpoint } from "./interfaces/endpoints/models";
|
|
5
|
+
import { PropsEndpoint } from "./interfaces/endpoints/props";
|
|
6
|
+
import { BaseModel } from "./models/baseModel";
|
|
7
|
+
import { LegacyModel } from "./models/legacyModel";
|
|
8
|
+
import { RouterModel } from "./models/routerModel";
|
|
9
|
+
import { SingleModel } from "./models/singleModel";
|
|
10
|
+
import { ConfigResolver } from "./resolver";
|
|
11
|
+
|
|
12
|
+
export class Server {
|
|
13
|
+
readonly models: BaseModel[] = [];
|
|
14
|
+
|
|
15
|
+
constructor(readonly baseUrl: string) {}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generates a unique provider ID from a server URL.
|
|
19
|
+
*/
|
|
20
|
+
get providerId(): string {
|
|
21
|
+
return `${PROVIDER_PREFIX}=${this.baseUrl}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Generates a human-readable provider name from a server URL.
|
|
26
|
+
*/
|
|
27
|
+
get providerName(): string {
|
|
28
|
+
return `${PROVIDER_NAME} (${this.baseUrl})`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Retrieves the API key from the resolver
|
|
33
|
+
* @returns The API key
|
|
34
|
+
*/
|
|
35
|
+
async getApiKey(): Promise<string> {
|
|
36
|
+
return await new ConfigResolver().resolveApiKey(this.providerId);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Fetches models from the server and populates {@link models}
|
|
41
|
+
*/
|
|
42
|
+
async initialize() {
|
|
43
|
+
const { data } = await this.fetchModels();
|
|
44
|
+
const mode = await this.detectServerMode();
|
|
45
|
+
|
|
46
|
+
// Setup models
|
|
47
|
+
const modelCtor = {
|
|
48
|
+
[Mode.ROUTER]: RouterModel,
|
|
49
|
+
[Mode.LEGACY]: LegacyModel,
|
|
50
|
+
[Mode.SINGLE]: SingleModel,
|
|
51
|
+
}[mode];
|
|
52
|
+
|
|
53
|
+
const models: BaseModel[] = data
|
|
54
|
+
.map((m) => new modelCtor(m, this))
|
|
55
|
+
.sort((a, b) => (a.id > b.id ? 1 : a.id === b.id ? 0 : -1));
|
|
56
|
+
|
|
57
|
+
this.models.length = 0;
|
|
58
|
+
this.models.push(...models);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Detects the mode of the server
|
|
63
|
+
*
|
|
64
|
+
* @returns The detected mode
|
|
65
|
+
*/
|
|
66
|
+
private async detectServerMode(): Promise<Mode> {
|
|
67
|
+
const { role } = await this.fetchServerProps();
|
|
68
|
+
const { data } = await this.fetchModels();
|
|
69
|
+
|
|
70
|
+
if (role === "router") return Mode.ROUTER;
|
|
71
|
+
if ("max_model_len" in data[0]) return Mode.LEGACY;
|
|
72
|
+
return Mode.SINGLE;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Detects if the server is ready
|
|
77
|
+
* @returns True if it's ready to work
|
|
78
|
+
*/
|
|
79
|
+
async isReady(): Promise<boolean> {
|
|
80
|
+
try {
|
|
81
|
+
const { status } = await this.fetchServerHealth();
|
|
82
|
+
return status === "ok";
|
|
83
|
+
} catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Retrieves the health status of the server
|
|
90
|
+
*
|
|
91
|
+
* @returns The health status
|
|
92
|
+
*/
|
|
93
|
+
async fetchServerHealth(): Promise<HealthEndpoint> {
|
|
94
|
+
return await this.rpc<HealthEndpoint>("/health");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Fetches models from the server
|
|
99
|
+
*
|
|
100
|
+
* @return The models from the server
|
|
101
|
+
*/
|
|
102
|
+
async fetchModels(): Promise<ModelsEndpoint> {
|
|
103
|
+
return await this.rpc<ModelsEndpoint>("/v1/models");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Fetches general properties of the server
|
|
108
|
+
*
|
|
109
|
+
* @return The properties of the server
|
|
110
|
+
*/
|
|
111
|
+
async fetchServerProps(): Promise<PropsEndpoint> {
|
|
112
|
+
return await this.rpc<PropsEndpoint>("/props?autoload=false");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Fetches properties of a specific model from the server
|
|
117
|
+
*
|
|
118
|
+
* @param modelId The ID of the model
|
|
119
|
+
* @return The properties of the specified model
|
|
120
|
+
*/
|
|
121
|
+
async fetchModelProps(modelId: string): Promise<PropsEndpoint> {
|
|
122
|
+
return await this.rpc<PropsEndpoint>(
|
|
123
|
+
`/props?model=${modelId}&autoload=false`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Sends a request associated to a specific model from the server
|
|
129
|
+
*
|
|
130
|
+
* @param resource The specified resource ("load" | "unload")
|
|
131
|
+
* @param model The targeted model
|
|
132
|
+
*/
|
|
133
|
+
async postRequest(
|
|
134
|
+
resource: "load" | "unload",
|
|
135
|
+
model: string,
|
|
136
|
+
): Promise<ModelsEndpoint> {
|
|
137
|
+
return await this.rpc<ModelsEndpoint>(`/models/${resource}`, { model });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Makes an HTTP request to the llama-server and returns the parsed JSON response
|
|
142
|
+
*
|
|
143
|
+
* @param endpoint The endpoint path to fetch (e.g. "/health")
|
|
144
|
+
* @param body The optional request body for POST requests
|
|
145
|
+
* @returns The parsed JSON response from the server
|
|
146
|
+
*/
|
|
147
|
+
private async rpc<T>(
|
|
148
|
+
endpoint: string,
|
|
149
|
+
body?: Record<string, unknown>,
|
|
150
|
+
): Promise<T> {
|
|
151
|
+
const url = `${this.baseUrl}${endpoint}`;
|
|
152
|
+
const apiKey = await this.getApiKey();
|
|
153
|
+
|
|
154
|
+
const data = {
|
|
155
|
+
method: body ? "POST" : "GET",
|
|
156
|
+
headers: body ? { "Content-Type": "application/json" } : undefined,
|
|
157
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const res = await fetch(url, {
|
|
161
|
+
...data,
|
|
162
|
+
headers: {
|
|
163
|
+
...data.headers,
|
|
164
|
+
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const response: T = await res.json();
|
|
169
|
+
return response;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -1,153 +1,202 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
import { CommandManager } from "../src/
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
resolveUrl: vi.fn(),
|
|
13
|
-
resolveApiKey: vi.fn(),
|
|
14
|
-
}));
|
|
15
|
-
|
|
16
|
-
// Import mocked functions after vi.mock
|
|
17
|
-
import { resolveApiKey, resolveUrl } from "../src/tools/resolver";
|
|
18
|
-
import { isServerReady, listModels } from "../src/tools/retriever";
|
|
19
|
-
|
|
20
|
-
const mockPi = {
|
|
21
|
-
registerProvider: vi.fn(),
|
|
22
|
-
};
|
|
2
|
+
import { Action } from "../src/enums/action";
|
|
3
|
+
import { CommandManager } from "../src/managers/command";
|
|
4
|
+
import { ServerManager } from "../src/managers/server";
|
|
5
|
+
import {
|
|
6
|
+
createMockCtx,
|
|
7
|
+
createMockModel,
|
|
8
|
+
createMockPi,
|
|
9
|
+
createMockServer,
|
|
10
|
+
mockRpc,
|
|
11
|
+
} from "./mocks";
|
|
23
12
|
|
|
24
13
|
beforeEach(() => {
|
|
25
14
|
vi.clearAllMocks();
|
|
26
|
-
|
|
27
|
-
(resolveApiKey as any).mockResolvedValue("test-key");
|
|
15
|
+
mockRpc.mockResolvedValue({ data: [] });
|
|
28
16
|
});
|
|
29
17
|
|
|
30
18
|
describe("CommandManager", () => {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
baseUrl: "http://127.0.0.1:8080",
|
|
40
|
-
api: "openai-completions",
|
|
41
|
-
apiKey: "test-key",
|
|
42
|
-
models: [],
|
|
43
|
-
});
|
|
19
|
+
let serverManager: ServerManager;
|
|
20
|
+
let commandManager: CommandManager;
|
|
21
|
+
let mockPi: ReturnType<typeof createMockPi>;
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
mockPi = createMockPi();
|
|
25
|
+
serverManager = new ServerManager([]);
|
|
26
|
+
commandManager = new CommandManager(serverManager);
|
|
44
27
|
});
|
|
45
28
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
.fn()
|
|
52
|
-
.mockResolvedValue({ id: "test-model", maxTokens: 32000 }),
|
|
53
|
-
};
|
|
54
|
-
(isServerReady as any).mockResolvedValue(true);
|
|
55
|
-
(listModels as any).mockResolvedValue([mockModel]);
|
|
56
|
-
|
|
57
|
-
const manager = new CommandManager(mockPi as any);
|
|
58
|
-
await manager.initialize();
|
|
59
|
-
|
|
60
|
-
expect(resolveUrl).toHaveBeenCalledWith(expect.any(String));
|
|
61
|
-
expect(listModels).toHaveBeenCalled();
|
|
62
|
-
expect(mockPi.registerProvider).toHaveBeenCalledWith(PROVIDER_ID, {
|
|
63
|
-
name: PROVIDER_NAME,
|
|
64
|
-
baseUrl: "http://127.0.0.1:8080",
|
|
65
|
-
api: "openai-completions",
|
|
66
|
-
apiKey: "test-key",
|
|
67
|
-
models: [{ id: "test-model", maxTokens: 32000 }],
|
|
29
|
+
describe("getArgumentCompletions", () => {
|
|
30
|
+
it("should provide completions for /models", () => {
|
|
31
|
+
const completions = commandManager.getArgumentCompletions("");
|
|
32
|
+
expect(completions).toHaveLength(2);
|
|
33
|
+
expect(completions?.map((c) => c.value)).toEqual(["info", "unload"]);
|
|
68
34
|
});
|
|
69
|
-
});
|
|
70
35
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
36
|
+
it("should filter completions by prefix", () => {
|
|
37
|
+
const completions = commandManager.getArgumentCompletions("u");
|
|
38
|
+
expect(completions).toHaveLength(1);
|
|
39
|
+
expect(completions?.[0].value).toBe("unload");
|
|
40
|
+
});
|
|
76
41
|
|
|
77
|
-
|
|
42
|
+
it("should return null when no completions match", () => {
|
|
43
|
+
const completions = commandManager.getArgumentCompletions("zzz");
|
|
44
|
+
expect(completions).toBeNull();
|
|
45
|
+
});
|
|
78
46
|
});
|
|
79
47
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
48
|
+
describe("handleCommand", () => {
|
|
49
|
+
it("should unload all models when args is 'unload'", async () => {
|
|
50
|
+
const model1 = createMockModel("model-1");
|
|
51
|
+
const model2 = createMockModel("model-2");
|
|
52
|
+
const server = createMockServer({
|
|
53
|
+
baseUrl: "http://127.0.0.1:8080",
|
|
54
|
+
models: [model1, model2],
|
|
55
|
+
});
|
|
56
|
+
serverManager = new ServerManager([server] as any);
|
|
57
|
+
commandManager = new CommandManager(serverManager);
|
|
58
|
+
|
|
59
|
+
const ctx = {
|
|
60
|
+
ui: {
|
|
61
|
+
notify: vi.fn(),
|
|
62
|
+
theme: { fg: (_: string, text: string) => text },
|
|
63
|
+
},
|
|
64
|
+
} as any;
|
|
65
|
+
|
|
66
|
+
await commandManager.handleCommand("unload", ctx, mockPi as any);
|
|
67
|
+
|
|
68
|
+
expect(model1.unload).toHaveBeenCalled();
|
|
69
|
+
expect(model2.unload).toHaveBeenCalled();
|
|
70
|
+
expect(ctx.ui.notify).toHaveBeenCalledWith(
|
|
71
|
+
"Unloaded all Llama.cpp models",
|
|
72
|
+
"info",
|
|
73
|
+
);
|
|
74
|
+
});
|
|
101
75
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
expect(mockModel1.unload).toHaveBeenCalled();
|
|
126
|
-
expect(mockModel2.unload).toHaveBeenCalled();
|
|
127
|
-
expect(notifyFn).toHaveBeenCalledWith(
|
|
128
|
-
"Unloaded all Llama.cpp models",
|
|
129
|
-
"info",
|
|
130
|
-
);
|
|
76
|
+
it("should show model info when args is 'info'", async () => {
|
|
77
|
+
const model1 = createMockModel("model-1");
|
|
78
|
+
const model2 = createMockModel("model-2");
|
|
79
|
+
const server = createMockServer({
|
|
80
|
+
baseUrl: "http://127.0.0.1:8080",
|
|
81
|
+
models: [model1, model2],
|
|
82
|
+
});
|
|
83
|
+
serverManager = new ServerManager([server] as any);
|
|
84
|
+
commandManager = new CommandManager(serverManager);
|
|
85
|
+
|
|
86
|
+
const ctx = {
|
|
87
|
+
ui: {
|
|
88
|
+
notify: vi.fn(),
|
|
89
|
+
theme: { fg: (_: string, text: string) => text },
|
|
90
|
+
},
|
|
91
|
+
} as any;
|
|
92
|
+
|
|
93
|
+
await commandManager.handleCommand("info", ctx, mockPi as any);
|
|
94
|
+
|
|
95
|
+
expect(model1.getInfo).toHaveBeenCalled();
|
|
96
|
+
expect(model2.getInfo).toHaveBeenCalled();
|
|
97
|
+
});
|
|
131
98
|
});
|
|
132
99
|
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
100
|
+
describe("/models interactive menu", () => {
|
|
101
|
+
const CHOICE = "model-a [Server: http://127.0.0.1:8080]";
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Helper to create a CommandManager with mock servers and models.
|
|
105
|
+
*/
|
|
106
|
+
const createCommandManager = (
|
|
107
|
+
models: ReturnType<typeof createMockModel>[],
|
|
108
|
+
) => {
|
|
109
|
+
const mockPi = createMockPi();
|
|
110
|
+
const servers = models.map((model) =>
|
|
111
|
+
createMockServer({
|
|
112
|
+
baseUrl: model.serverUrl,
|
|
113
|
+
models: [model],
|
|
114
|
+
}),
|
|
115
|
+
);
|
|
116
|
+
const serverManager = new ServerManager(servers as any);
|
|
117
|
+
return {
|
|
118
|
+
commandManager: new CommandManager(serverManager),
|
|
119
|
+
serverManager,
|
|
120
|
+
mockPi,
|
|
121
|
+
};
|
|
139
122
|
};
|
|
140
|
-
|
|
141
|
-
(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
123
|
+
|
|
124
|
+
it("should return early on cancel (null model selection)", async () => {
|
|
125
|
+
const models = [createMockModel("model-a")];
|
|
126
|
+
const { commandManager, mockPi } = createCommandManager(models);
|
|
127
|
+
const ctx = createMockCtx(() => null);
|
|
128
|
+
|
|
129
|
+
await commandManager.handleCommand("", ctx as any, mockPi as any);
|
|
130
|
+
|
|
131
|
+
expect(ctx.ui.notify).not.toHaveBeenCalled();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("should show info when INFO action is selected", async () => {
|
|
135
|
+
const model = createMockModel("model-a");
|
|
136
|
+
const { commandManager, mockPi } = createCommandManager([model]);
|
|
137
|
+
let selectCallCount = 0;
|
|
138
|
+
const ctx = createMockCtx(() => {
|
|
139
|
+
selectCallCount++;
|
|
140
|
+
if (selectCallCount === 1) return CHOICE;
|
|
141
|
+
return Action.INFO;
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
await commandManager.handleCommand("", ctx as any, mockPi as any);
|
|
145
|
+
|
|
146
|
+
expect(ctx.ui.notify).toHaveBeenCalledWith(
|
|
147
|
+
"Model: model-a\nID: model-a",
|
|
148
|
+
"info",
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("should unload model when UNLOAD action is selected", async () => {
|
|
153
|
+
const model = createMockModel("model-a");
|
|
154
|
+
const { commandManager, mockPi } = createCommandManager([model]);
|
|
155
|
+
let selectCallCount = 0;
|
|
156
|
+
const ctx = createMockCtx(() => {
|
|
157
|
+
selectCallCount++;
|
|
158
|
+
if (selectCallCount === 1) return CHOICE;
|
|
159
|
+
return Action.UNLOAD;
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
await commandManager.handleCommand("", ctx as any, mockPi as any);
|
|
163
|
+
|
|
164
|
+
expect(model.unload).toHaveBeenCalled();
|
|
165
|
+
expect(ctx.ui.notify).toHaveBeenCalledWith("Unloaded model-a", "info");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("should switch model when SWITCH action is selected", async () => {
|
|
169
|
+
const model = createMockModel("model-a");
|
|
170
|
+
const { commandManager, mockPi } = createCommandManager([model]);
|
|
171
|
+
let selectCallCount = 0;
|
|
172
|
+
const ctx = createMockCtx(() => {
|
|
173
|
+
selectCallCount++;
|
|
174
|
+
if (selectCallCount === 1) return CHOICE;
|
|
175
|
+
return Action.SWITCH;
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
await commandManager.handleCommand("", ctx as any, mockPi as any);
|
|
179
|
+
|
|
180
|
+
expect(mockPi.setModel).toHaveBeenCalled();
|
|
181
|
+
expect(ctx.ui.notify).toHaveBeenCalledWith("Model model-a ready", "info");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("should loop back to model selection when action is cancelled", async () => {
|
|
185
|
+
const model = createMockModel("model-a");
|
|
186
|
+
const { commandManager, mockPi } = createCommandManager([model]);
|
|
187
|
+
|
|
188
|
+
let selectCallCount = 0;
|
|
189
|
+
const ctx = createMockCtx(() => {
|
|
190
|
+
selectCallCount++;
|
|
191
|
+
// 1st: select model-a, 2nd: cancel action, 3rd: cancel model => exit
|
|
192
|
+
if (selectCallCount === 1) return CHOICE;
|
|
193
|
+
return null;
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
await commandManager.handleCommand("", ctx as any, mockPi as any);
|
|
197
|
+
|
|
198
|
+
expect(ctx.ui.select).toHaveBeenCalledTimes(3);
|
|
199
|
+
expect(ctx.ui.notify).not.toHaveBeenCalled();
|
|
200
|
+
});
|
|
152
201
|
});
|
|
153
202
|
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { Mode } from "../src/enums/mode";
|
|
3
|
+
import { Status } from "../src/enums/status";
|
|
4
|
+
import { DataProperty } from "../src/interfaces/endpoints/models";
|
|
5
|
+
import { LegacyModel } from "../src/models/legacyModel";
|
|
6
|
+
import { createMockServer, mockRpc } from "./mocks";
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
mockRpc.mockReset();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const createModel = (extra: Partial<DataProperty> = {}): LegacyModel =>
|
|
13
|
+
new LegacyModel(
|
|
14
|
+
{
|
|
15
|
+
id: "test",
|
|
16
|
+
tags: [],
|
|
17
|
+
object: "model",
|
|
18
|
+
owned_by: "test",
|
|
19
|
+
created: Date.now(),
|
|
20
|
+
...extra,
|
|
21
|
+
},
|
|
22
|
+
createMockServer(),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
describe("LegacyModel mode", () => {
|
|
26
|
+
it("should always return LEGACY mode", () => {
|
|
27
|
+
const model = createModel();
|
|
28
|
+
expect(model.mode).toBe(Mode.LEGACY);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("LegacyModel capabilities", () => {
|
|
33
|
+
it("should detect image capability when multimodal is in capabilities", async () => {
|
|
34
|
+
mockRpc.mockResolvedValueOnce({ modalities: { vision: true } });
|
|
35
|
+
|
|
36
|
+
const model = createModel();
|
|
37
|
+
const capabilities = await model.getCapabilities();
|
|
38
|
+
|
|
39
|
+
expect(capabilities).toEqual(["text", "image"]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should detect text-only capability when multimodal is not in capabilities", async () => {
|
|
43
|
+
mockRpc.mockResolvedValueOnce({ modalities: { vision: false } });
|
|
44
|
+
|
|
45
|
+
const model = createModel();
|
|
46
|
+
const capabilities = await model.getCapabilities();
|
|
47
|
+
|
|
48
|
+
expect(capabilities).toEqual(["text"]);
|
|
49
|
+
});
|
|
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
|
+
|
|
59
|
+
expect(status).toBe(Status.LOADED);
|
|
60
|
+
expect(mockRpc).toHaveBeenCalledWith(
|
|
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 });
|
|
67
|
+
|
|
68
|
+
const model = createModel();
|
|
69
|
+
const status = await model.getStatus();
|
|
70
|
+
|
|
71
|
+
expect(status).toBe(Status.SLEEPING);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("LegacyModel getContextSize", () => {
|
|
76
|
+
it("should use max_model_len when it is non-zero", async () => {
|
|
77
|
+
mockRpc.mockResolvedValueOnce({ n_ctx: 4096 });
|
|
78
|
+
mockRpc.mockResolvedValueOnce({
|
|
79
|
+
data: [{ max_model_len: 8192 }],
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const model = createModel();
|
|
83
|
+
const ctxSize = await model.getContextSize();
|
|
84
|
+
|
|
85
|
+
expect(ctxSize).toBe(8192);
|
|
86
|
+
expect(mockRpc).toHaveBeenCalledWith("/v1/models");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("should fall back to n_ctx when max_model_len is 0", async () => {
|
|
90
|
+
mockRpc.mockResolvedValueOnce({ n_ctx: 4096 });
|
|
91
|
+
mockRpc.mockResolvedValueOnce({
|
|
92
|
+
data: [{ max_model_len: 0 }],
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const model = createModel();
|
|
96
|
+
const ctxSize = await model.getContextSize();
|
|
97
|
+
|
|
98
|
+
expect(ctxSize).toBe(4096);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("should return DEFAULT_CTX when both values are missing/null", async () => {
|
|
102
|
+
mockRpc.mockResolvedValueOnce({});
|
|
103
|
+
mockRpc.mockResolvedValueOnce({
|
|
104
|
+
data: [{ max_model_len: null }],
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const model = createModel();
|
|
108
|
+
const ctxSize = await model.getContextSize();
|
|
109
|
+
|
|
110
|
+
expect(ctxSize).toBe(128000);
|
|
111
|
+
});
|
|
112
|
+
});
|