pi-llama-cpp 0.5.1 → 0.7.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 +96 -30
- package/package.json +6 -5
- package/src/constants.ts +27 -5
- package/src/enums/action.ts +3 -2
- package/src/enums/mode.ts +1 -0
- package/src/enums/serverStatus.ts +6 -0
- package/src/enums/status.ts +1 -0
- package/src/index.ts +53 -31
- package/src/interfaces/auth.ts +1 -5
- package/src/interfaces/endpoints/props.ts +1 -0
- package/src/interfaces/levels.ts +7 -0
- package/src/managers/command.ts +290 -0
- package/src/managers/events.ts +101 -0
- package/src/managers/server.ts +136 -0
- package/src/models/baseModel.ts +75 -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 +152 -0
- package/src/server.ts +187 -0
- package/tests/commandManager.test.ts +182 -133
- package/tests/events.test.ts +256 -0
- package/tests/legacyModel.test.ts +112 -0
- package/tests/mocks.ts +100 -0
- package/tests/resolver.test.ts +143 -106
- package/tests/routerModel.test.ts +46 -68
- package/tests/server.test.ts +176 -0
- package/tests/serverManager.test.ts +130 -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
|
@@ -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,256 @@
|
|
|
1
|
+
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { DEFAULT_THINKING_BUDGETS } from "../src/constants";
|
|
3
|
+
import { createMockModel, createMockServer } from "./mocks";
|
|
4
|
+
|
|
5
|
+
// Create a mutable mock object shared across tests
|
|
6
|
+
const mockSettingsManager = {
|
|
7
|
+
getDefaultThinkingLevel: vi.fn(() => "medium"),
|
|
8
|
+
getThinkingBudgets: vi.fn<() => Record<string, number> | undefined>(),
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
vi.mock("@earendil-works/pi-coding-agent", async (importOriginal) => {
|
|
12
|
+
const actual =
|
|
13
|
+
await importOriginal<typeof import("@earendil-works/pi-coding-agent")>();
|
|
14
|
+
return {
|
|
15
|
+
...actual,
|
|
16
|
+
SettingsManager: {
|
|
17
|
+
create: () => mockSettingsManager,
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
let EventManager: typeof import("../src/managers/events").EventManager;
|
|
23
|
+
|
|
24
|
+
beforeAll(async () => {
|
|
25
|
+
const mod = await vi.importActual("../src/managers/events");
|
|
26
|
+
EventManager =
|
|
27
|
+
mod.EventManager as typeof import("../src/managers/events").EventManager;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
vi.restoreAllMocks();
|
|
32
|
+
EventManager.resetInflightModel();
|
|
33
|
+
mockSettingsManager.getDefaultThinkingLevel.mockReturnValue("medium");
|
|
34
|
+
mockSettingsManager.getThinkingBudgets.mockReturnValue(undefined);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const createPayload = (modelId: string) => ({
|
|
38
|
+
model: modelId,
|
|
39
|
+
messages: [{ role: "user", content: "hello" }],
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const createNonLlamaPayload = () => ({
|
|
43
|
+
model: "gpt-4",
|
|
44
|
+
messages: [{ role: "user", content: "hello" }],
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("EventManager.onBeforeProviderRequest", () => {
|
|
48
|
+
describe("normal usage — each thinking level", () => {
|
|
49
|
+
it.each([
|
|
50
|
+
{
|
|
51
|
+
level: "off",
|
|
52
|
+
expected: { chat_template_kwargs: { enable_thinking: false } },
|
|
53
|
+
},
|
|
54
|
+
{ level: "minimal", expected: { thinking_budget_tokens: 1024 } },
|
|
55
|
+
{ level: "low", expected: { thinking_budget_tokens: 2048 } },
|
|
56
|
+
{ level: "medium", expected: { thinking_budget_tokens: 8192 } },
|
|
57
|
+
{ level: "high", expected: { thinking_budget_tokens: 16384 } },
|
|
58
|
+
{ level: "xhigh", expected: {} },
|
|
59
|
+
])(
|
|
60
|
+
'level "$level" should return $expected',
|
|
61
|
+
async ({ level, expected }) => {
|
|
62
|
+
mockSettingsManager.getDefaultThinkingLevel.mockReturnValue(level);
|
|
63
|
+
|
|
64
|
+
const server = createMockServer({
|
|
65
|
+
models: ["model-a"].map((id) => createMockModel(id)),
|
|
66
|
+
});
|
|
67
|
+
const eventManager = new EventManager([server]);
|
|
68
|
+
const event = { payload: createPayload("model-a") };
|
|
69
|
+
|
|
70
|
+
const result = (await eventManager.onBeforeProviderRequest(
|
|
71
|
+
event as any,
|
|
72
|
+
)) as Record<string, unknown>;
|
|
73
|
+
|
|
74
|
+
expect(result.model).toBe("model-a");
|
|
75
|
+
expect(result).toMatchObject(expected);
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
it("should preserve original payload fields alongside new ones", async () => {
|
|
80
|
+
mockSettingsManager.getDefaultThinkingLevel.mockReturnValue("low");
|
|
81
|
+
|
|
82
|
+
const server = createMockServer({
|
|
83
|
+
models: ["model-b"].map((id) => createMockModel(id)),
|
|
84
|
+
});
|
|
85
|
+
const eventManager = new EventManager([server]);
|
|
86
|
+
const event = {
|
|
87
|
+
payload: {
|
|
88
|
+
model: "model-b",
|
|
89
|
+
messages: [{ role: "user", content: "test" }],
|
|
90
|
+
temperature: 0.7,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const result = (await eventManager.onBeforeProviderRequest(
|
|
95
|
+
event as any,
|
|
96
|
+
)) as Record<string, unknown>;
|
|
97
|
+
|
|
98
|
+
expect(result.messages).toEqual([{ role: "user", content: "test" }]);
|
|
99
|
+
expect(result.temperature).toBe(0.7);
|
|
100
|
+
expect(result.thinking_budget_tokens).toBe(DEFAULT_THINKING_BUDGETS.low);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("non-llama.cpp models", () => {
|
|
105
|
+
it("should return the payload unchanged for unknown models", async () => {
|
|
106
|
+
const server = createMockServer({
|
|
107
|
+
models: ["model-a"].map((id) => createMockModel(id)),
|
|
108
|
+
});
|
|
109
|
+
const eventManager = new EventManager([server]);
|
|
110
|
+
const event = { payload: createNonLlamaPayload() };
|
|
111
|
+
|
|
112
|
+
const result = await eventManager.onBeforeProviderRequest(event as any);
|
|
113
|
+
|
|
114
|
+
expect(result).toEqual(createNonLlamaPayload());
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("missing model in payload", () => {
|
|
119
|
+
it("should return the payload unchanged when model is absent", async () => {
|
|
120
|
+
const server = createMockServer({
|
|
121
|
+
models: ["model-a"].map((id) => createMockModel(id)),
|
|
122
|
+
});
|
|
123
|
+
const eventManager = new EventManager([server]);
|
|
124
|
+
const event = { payload: { messages: [] } };
|
|
125
|
+
|
|
126
|
+
const result = await eventManager.onBeforeProviderRequest(event as any);
|
|
127
|
+
|
|
128
|
+
expect(result).toEqual({ messages: [] });
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("user-defined budget overrides", () => {
|
|
133
|
+
it("should use user-defined budgets instead of defaults", async () => {
|
|
134
|
+
mockSettingsManager.getDefaultThinkingLevel.mockReturnValue("low");
|
|
135
|
+
mockSettingsManager.getThinkingBudgets.mockReturnValue({ low: 4096 });
|
|
136
|
+
|
|
137
|
+
const server = createMockServer({
|
|
138
|
+
models: ["model-a"].map((id) => createMockModel(id)),
|
|
139
|
+
});
|
|
140
|
+
const eventManager = new EventManager([server]);
|
|
141
|
+
const event = { payload: createPayload("model-a") };
|
|
142
|
+
|
|
143
|
+
const result = (await eventManager.onBeforeProviderRequest(
|
|
144
|
+
event as any,
|
|
145
|
+
)) as Record<string, unknown>;
|
|
146
|
+
|
|
147
|
+
expect(result.thinking_budget_tokens).toBe(4096);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("should merge user budgets with defaults (partial override)", async () => {
|
|
151
|
+
mockSettingsManager.getDefaultThinkingLevel.mockReturnValue("medium");
|
|
152
|
+
mockSettingsManager.getThinkingBudgets.mockReturnValue({ low: 4096 });
|
|
153
|
+
|
|
154
|
+
const server = createMockServer({
|
|
155
|
+
models: ["model-a"].map((id) => createMockModel(id)),
|
|
156
|
+
});
|
|
157
|
+
const eventManager = new EventManager([server]);
|
|
158
|
+
const event = { payload: createPayload("model-a") };
|
|
159
|
+
|
|
160
|
+
const result = (await eventManager.onBeforeProviderRequest(
|
|
161
|
+
event as any,
|
|
162
|
+
)) as Record<string, unknown>;
|
|
163
|
+
|
|
164
|
+
// medium uses default since user only overrode low
|
|
165
|
+
expect(result.thinking_budget_tokens).toBe(
|
|
166
|
+
DEFAULT_THINKING_BUDGETS.medium,
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// ─── Edge cases ─────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
describe("edge cases", () => {
|
|
174
|
+
it("should ignore invalid keys in user budgets (they are silently dropped)", async () => {
|
|
175
|
+
mockSettingsManager.getDefaultThinkingLevel.mockReturnValue("medium");
|
|
176
|
+
mockSettingsManager.getThinkingBudgets.mockReturnValue({
|
|
177
|
+
foo: 999,
|
|
178
|
+
bar: 123,
|
|
179
|
+
} as any);
|
|
180
|
+
|
|
181
|
+
const server = createMockServer({
|
|
182
|
+
models: ["model-a"].map((id) => createMockModel(id)),
|
|
183
|
+
});
|
|
184
|
+
const eventManager = new EventManager([server]);
|
|
185
|
+
const event = { payload: createPayload("model-a") };
|
|
186
|
+
|
|
187
|
+
const result = (await eventManager.onBeforeProviderRequest(
|
|
188
|
+
event as any,
|
|
189
|
+
)) as Record<string, unknown>;
|
|
190
|
+
|
|
191
|
+
// Should fall back to default since "medium" is not in user budgets
|
|
192
|
+
expect(result.thinking_budget_tokens).toBe(
|
|
193
|
+
DEFAULT_THINKING_BUDGETS.medium,
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("should not allow overriding 'off' — thinking stays disabled", async () => {
|
|
198
|
+
mockSettingsManager.getDefaultThinkingLevel.mockReturnValue("off");
|
|
199
|
+
mockSettingsManager.getThinkingBudgets.mockReturnValue({
|
|
200
|
+
off: 99999,
|
|
201
|
+
} as any);
|
|
202
|
+
|
|
203
|
+
const server = createMockServer({
|
|
204
|
+
models: ["model-a"].map((id) => createMockModel(id)),
|
|
205
|
+
});
|
|
206
|
+
const eventManager = new EventManager([server]);
|
|
207
|
+
const event = { payload: createPayload("model-a") };
|
|
208
|
+
|
|
209
|
+
const result = (await eventManager.onBeforeProviderRequest(
|
|
210
|
+
event as any,
|
|
211
|
+
)) as Record<string, unknown>;
|
|
212
|
+
|
|
213
|
+
expect(result).toMatchObject({
|
|
214
|
+
chat_template_kwargs: { enable_thinking: false },
|
|
215
|
+
});
|
|
216
|
+
expect(result).not.toHaveProperty("thinking_budget_tokens");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("should not allow overriding 'xhigh' — no budget is injected", async () => {
|
|
220
|
+
mockSettingsManager.getDefaultThinkingLevel.mockReturnValue("xhigh");
|
|
221
|
+
mockSettingsManager.getThinkingBudgets.mockReturnValue({
|
|
222
|
+
xhigh: 1,
|
|
223
|
+
} as any);
|
|
224
|
+
|
|
225
|
+
const server = createMockServer({
|
|
226
|
+
models: ["model-a"].map((id) => createMockModel(id)),
|
|
227
|
+
});
|
|
228
|
+
const eventManager = new EventManager([server]);
|
|
229
|
+
const event = { payload: createPayload("model-a") };
|
|
230
|
+
|
|
231
|
+
const result = (await eventManager.onBeforeProviderRequest(
|
|
232
|
+
event as any,
|
|
233
|
+
)) as Record<string, unknown>;
|
|
234
|
+
|
|
235
|
+
expect(result).toEqual(createPayload("model-a"));
|
|
236
|
+
expect(result).not.toHaveProperty("thinking_budget_tokens");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("should handle empty user budgets gracefully", async () => {
|
|
240
|
+
mockSettingsManager.getDefaultThinkingLevel.mockReturnValue("high");
|
|
241
|
+
mockSettingsManager.getThinkingBudgets.mockReturnValue({});
|
|
242
|
+
|
|
243
|
+
const server = createMockServer({
|
|
244
|
+
models: ["model-a"].map((id) => createMockModel(id)),
|
|
245
|
+
});
|
|
246
|
+
const eventManager = new EventManager([server]);
|
|
247
|
+
const event = { payload: createPayload("model-a") };
|
|
248
|
+
|
|
249
|
+
const result = (await eventManager.onBeforeProviderRequest(
|
|
250
|
+
event as any,
|
|
251
|
+
)) as Record<string, unknown>;
|
|
252
|
+
|
|
253
|
+
expect(result.thinking_budget_tokens).toBe(DEFAULT_THINKING_BUDGETS.high);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
});
|