pi-llama-cpp 0.9.2 → 0.11.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 +163 -27
- package/package.json +4 -3
- package/src/api/client.ts +76 -32
- package/src/constants.ts +28 -3
- package/src/index.ts +4 -12
- package/src/interfaces/events.ts +14 -3
- package/src/interfaces/server.ts +20 -0
- package/src/interfaces/settings.ts +69 -0
- package/src/managers/command.ts +285 -43
- package/src/managers/events.ts +50 -12
- package/src/managers/server.ts +90 -17
- package/src/managers/settings.ts +268 -0
- package/src/models/baseModel.ts +32 -17
- package/src/models/legacyModel.ts +2 -2
- package/src/models/routerModel.ts +3 -3
- package/src/server.ts +86 -21
- package/src/sse/client.ts +28 -16
- package/src/sse/manager.ts +23 -10
- package/src/ui/serverListEditor.ts +481 -0
- package/src/utils/errors.ts +5 -0
- package/src/utils/settingsStore.ts +60 -0
- package/src/utils/urls.ts +16 -0
- package/tests/commandManager.test.ts +256 -11
- package/tests/events.test.ts +229 -64
- package/tests/mocks.ts +145 -32
- package/tests/server.test.ts +54 -6
- package/tests/serverListEditor.test.ts +637 -0
- package/tests/serverManager.test.ts +282 -39
- package/tests/settings.test.ts +793 -0
- package/tests/settingsStore.test.ts +209 -0
- package/tests/sseManager.test.ts +98 -0
- package/src/interfaces/auth.ts +0 -6
- package/src/resolver.ts +0 -149
- package/src/utils/cache.ts +0 -39
- package/src/utils/mutex.ts +0 -24
- package/tests/resolver.test.ts +0 -184
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { SettingsStore } from "../src/utils/settingsStore";
|
|
4
|
+
|
|
5
|
+
vi.mock("@earendil-works/pi-coding-agent", () => ({
|
|
6
|
+
getAgentDir: vi.fn().mockReturnValue("/fake/agent/dir"),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
const mockReadFile = vi.hoisted(() => vi.fn());
|
|
10
|
+
const mockWriteFile = vi.hoisted(() => vi.fn());
|
|
11
|
+
const mockRename = vi.hoisted(() => vi.fn());
|
|
12
|
+
|
|
13
|
+
vi.mock("node:fs/promises", () => ({
|
|
14
|
+
readFile: mockReadFile,
|
|
15
|
+
writeFile: mockWriteFile,
|
|
16
|
+
rename: mockRename,
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
const mockGetAgentDir = vi.mocked(getAgentDir);
|
|
20
|
+
|
|
21
|
+
const SETTINGS_PATH = "/fake/agent/dir/settings.json";
|
|
22
|
+
|
|
23
|
+
describe("SettingsStore.read", () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
vi.clearAllMocks();
|
|
26
|
+
mockGetAgentDir.mockReturnValue("/fake/agent/dir");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should return {} when the file is missing (ENOENT)", async () => {
|
|
30
|
+
mockReadFile.mockRejectedValue(
|
|
31
|
+
Object.assign(new Error("nope"), { code: "ENOENT" }),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const store = new SettingsStore(SETTINGS_PATH);
|
|
35
|
+
|
|
36
|
+
await expect(store.read()).resolves.toEqual({});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("should reject with a message containing the path on invalid JSON", async () => {
|
|
40
|
+
mockReadFile.mockResolvedValue("{ not valid json");
|
|
41
|
+
|
|
42
|
+
const store = new SettingsStore(SETTINGS_PATH);
|
|
43
|
+
|
|
44
|
+
await expect(store.read()).rejects.toThrow(/Cannot parse.*settings\.json/);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("should propagate non-ENOENT IO errors", async () => {
|
|
48
|
+
mockReadFile.mockRejectedValue(
|
|
49
|
+
Object.assign(new Error("permission denied"), { code: "EACCES" }),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const store = new SettingsStore(SETTINGS_PATH);
|
|
53
|
+
|
|
54
|
+
await expect(store.read()).rejects.toMatchObject({ code: "EACCES" });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should default the path to <agentDir>/settings.json", async () => {
|
|
58
|
+
mockReadFile.mockResolvedValue('{"a":1}');
|
|
59
|
+
|
|
60
|
+
const store = new SettingsStore();
|
|
61
|
+
await store.read();
|
|
62
|
+
|
|
63
|
+
expect(mockReadFile).toHaveBeenCalledWith(SETTINGS_PATH, "utf-8");
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("SettingsStore.updateKey — file content", () => {
|
|
68
|
+
beforeEach(() => {
|
|
69
|
+
vi.clearAllMocks();
|
|
70
|
+
mockGetAgentDir.mockReturnValue("/fake/agent/dir");
|
|
71
|
+
mockWriteFile.mockResolvedValue(undefined);
|
|
72
|
+
mockRename.mockResolvedValue(undefined);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("should create the file with only the target key when the file was missing", async () => {
|
|
76
|
+
mockReadFile.mockRejectedValue(
|
|
77
|
+
Object.assign(new Error("nope"), { code: "ENOENT" }),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const store = new SettingsStore(SETTINGS_PATH);
|
|
81
|
+
await store.updateKey("llamaSettings", () => ({ sortBy: "asc" }));
|
|
82
|
+
|
|
83
|
+
const [, written] = mockWriteFile.mock.calls[0];
|
|
84
|
+
expect(JSON.parse(written as string)).toEqual({
|
|
85
|
+
llamaSettings: { sortBy: "asc" },
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("should preserve unrelated keys, their order, and merge into an existing target key", async () => {
|
|
90
|
+
mockReadFile.mockResolvedValue(
|
|
91
|
+
'{"aaa":1,"llamaSettings":{"sortBy":"asc"},"zzz":9}',
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const store = new SettingsStore(SETTINGS_PATH);
|
|
95
|
+
await store.updateKey("llamaSettings", (current) => ({
|
|
96
|
+
...(current as Record<string, unknown>),
|
|
97
|
+
sortBy: "desc",
|
|
98
|
+
}));
|
|
99
|
+
|
|
100
|
+
const [, written] = mockWriteFile.mock.calls[0];
|
|
101
|
+
const parsed = JSON.parse(written as string);
|
|
102
|
+
expect(Object.keys(parsed)).toEqual(["aaa", "llamaSettings", "zzz"]);
|
|
103
|
+
expect(parsed.llamaSettings).toEqual({ sortBy: "desc" });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("should start from {} when the target key holds a non-object (null)", async () => {
|
|
107
|
+
mockReadFile.mockResolvedValue('{"llamaSettings":null}');
|
|
108
|
+
|
|
109
|
+
const store = new SettingsStore(SETTINGS_PATH);
|
|
110
|
+
await store.updateKey("llamaSettings", (current) => ({
|
|
111
|
+
...(typeof current === "object" && current !== null ? current : {}),
|
|
112
|
+
autoloadOnMessage: true,
|
|
113
|
+
}));
|
|
114
|
+
|
|
115
|
+
const [, written] = mockWriteFile.mock.calls[0];
|
|
116
|
+
expect(JSON.parse(written as string)).toEqual({
|
|
117
|
+
llamaSettings: { autoloadOnMessage: true },
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("SettingsStore.updateKey — write path", () => {
|
|
123
|
+
beforeEach(() => {
|
|
124
|
+
vi.clearAllMocks();
|
|
125
|
+
mockGetAgentDir.mockReturnValue("/fake/agent/dir");
|
|
126
|
+
mockWriteFile.mockResolvedValue(undefined);
|
|
127
|
+
mockRename.mockResolvedValue(undefined);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("should write to <path>.tmp with 2-space indent, then rename onto the real path", async () => {
|
|
131
|
+
mockReadFile.mockResolvedValue('{"llamaSettings":{"sortBy":"asc"}}');
|
|
132
|
+
|
|
133
|
+
const store = new SettingsStore(SETTINGS_PATH);
|
|
134
|
+
await store.updateKey("llamaSettings", (current) => ({
|
|
135
|
+
...(current as Record<string, unknown>),
|
|
136
|
+
sortBy: "desc",
|
|
137
|
+
}));
|
|
138
|
+
|
|
139
|
+
expect(mockWriteFile).toHaveBeenCalledTimes(1);
|
|
140
|
+
expect(mockWriteFile).toHaveBeenCalledWith(
|
|
141
|
+
`${SETTINGS_PATH}.tmp`,
|
|
142
|
+
expect.stringContaining('\n "llamaSettings"'),
|
|
143
|
+
"utf-8",
|
|
144
|
+
);
|
|
145
|
+
expect(mockRename).toHaveBeenCalledTimes(1);
|
|
146
|
+
expect(mockRename).toHaveBeenCalledWith(
|
|
147
|
+
`${SETTINGS_PATH}.tmp`,
|
|
148
|
+
SETTINGS_PATH,
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("should apply queued concurrent writes in submission order (last write wins)", async () => {
|
|
153
|
+
// Simulate a real file: reads return the last written content.
|
|
154
|
+
let contents = "{}";
|
|
155
|
+
mockReadFile.mockImplementation(async () => contents);
|
|
156
|
+
mockWriteFile.mockImplementation(async (_path: unknown, data: string) => {
|
|
157
|
+
contents = data;
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const store = new SettingsStore(SETTINGS_PATH);
|
|
161
|
+
const first = store.updateKey("llamaSettings", (current) => ({
|
|
162
|
+
...(current as Record<string, unknown>),
|
|
163
|
+
first: 1,
|
|
164
|
+
}));
|
|
165
|
+
const second = store.updateKey("llamaSettings", (current) => ({
|
|
166
|
+
...(current as Record<string, unknown>),
|
|
167
|
+
second: 2,
|
|
168
|
+
}));
|
|
169
|
+
|
|
170
|
+
await first;
|
|
171
|
+
await second;
|
|
172
|
+
|
|
173
|
+
// Without the queue both writes would read the same "{}" and the final
|
|
174
|
+
// content would only hold the last caller's key.
|
|
175
|
+
expect(mockReadFile).toHaveBeenCalledTimes(2);
|
|
176
|
+
expect(mockWriteFile).toHaveBeenCalledTimes(2);
|
|
177
|
+
const [, firstWrite] = mockWriteFile.mock.calls[0];
|
|
178
|
+
const [, secondWrite] = mockWriteFile.mock.calls[1];
|
|
179
|
+
expect(JSON.parse(firstWrite as string)).toEqual({
|
|
180
|
+
llamaSettings: { first: 1 },
|
|
181
|
+
});
|
|
182
|
+
expect(JSON.parse(secondWrite as string)).toEqual({
|
|
183
|
+
llamaSettings: { first: 1, second: 2 },
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("should reject a failing write but let the next queued update still run", async () => {
|
|
188
|
+
mockReadFile.mockResolvedValue('{"llamaSettings":{}}');
|
|
189
|
+
mockWriteFile.mockRejectedValueOnce(new Error("ENOSPC: simulated"));
|
|
190
|
+
|
|
191
|
+
const store = new SettingsStore(SETTINGS_PATH);
|
|
192
|
+
const failing = store.updateKey("llamaSettings", (current) => ({
|
|
193
|
+
...(current as Record<string, unknown>),
|
|
194
|
+
a: 1,
|
|
195
|
+
}));
|
|
196
|
+
const following = store.updateKey("llamaSettings", (current) => ({
|
|
197
|
+
...(current as Record<string, unknown>),
|
|
198
|
+
b: 2,
|
|
199
|
+
}));
|
|
200
|
+
|
|
201
|
+
await expect(failing).rejects.toThrow("ENOSPC");
|
|
202
|
+
await expect(following).resolves.toBeUndefined();
|
|
203
|
+
|
|
204
|
+
const [, lastWrite] = mockWriteFile.mock.calls[1];
|
|
205
|
+
expect(JSON.parse(lastWrite as string)).toEqual({
|
|
206
|
+
llamaSettings: { b: 2 },
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { SSEManager } from "../src/sse/manager";
|
|
3
|
+
import { createMockServer } from "./mocks";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds an SSEManager bound to a stub server, mirroring how
|
|
7
|
+
* `Server.initialize()` wires it up (timeouts are read through the server).
|
|
8
|
+
*/
|
|
9
|
+
const createManager = (
|
|
10
|
+
pollingTimeout = 60000,
|
|
11
|
+
serverTimeout = 1000,
|
|
12
|
+
): SSEManager =>
|
|
13
|
+
new SSEManager(
|
|
14
|
+
createMockServer({
|
|
15
|
+
baseUrl: "http://127.0.0.1:8080",
|
|
16
|
+
pollingTimeout,
|
|
17
|
+
serverTimeout,
|
|
18
|
+
}),
|
|
19
|
+
"",
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
describe("SSEManager timeouts", () => {
|
|
23
|
+
it("should expose the timeouts of its server", () => {
|
|
24
|
+
const manager = createManager(5678, 1234);
|
|
25
|
+
|
|
26
|
+
expect(manager.pollingTimeout).toBe(5678);
|
|
27
|
+
expect(manager.serverTimeout).toBe(1234);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("SSEManager subscribeToStatus", () => {
|
|
32
|
+
it("should reject after the server's pollingTimeout, not the constant", async () => {
|
|
33
|
+
vi.useFakeTimers();
|
|
34
|
+
vi.stubGlobal(
|
|
35
|
+
"EventSource",
|
|
36
|
+
class StubEventSource {
|
|
37
|
+
onopen = null;
|
|
38
|
+
onerror = null;
|
|
39
|
+
onmessage = null;
|
|
40
|
+
close() {}
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const manager = createManager(5000);
|
|
46
|
+
const promise = manager.subscribeToStatus("test-model");
|
|
47
|
+
const expectation = expect(promise).rejects.toThrow(
|
|
48
|
+
"SSE status timeout for model: test-model",
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// If the timer still used the fixed 60 s constant, advancing 5 s
|
|
52
|
+
// would not trigger the rejection.
|
|
53
|
+
await vi.advanceTimersByTimeAsync(5000);
|
|
54
|
+
await expectation;
|
|
55
|
+
} finally {
|
|
56
|
+
vi.unstubAllGlobals();
|
|
57
|
+
vi.useRealTimers();
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should resolve when a terminal status arrives before the timeout", async () => {
|
|
62
|
+
vi.useFakeTimers();
|
|
63
|
+
vi.stubGlobal(
|
|
64
|
+
"EventSource",
|
|
65
|
+
class StubEventSource {
|
|
66
|
+
onopen = null;
|
|
67
|
+
onerror = null;
|
|
68
|
+
onmessage = null;
|
|
69
|
+
close() {}
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const manager = createManager(5000);
|
|
75
|
+
const promise = manager.subscribeToStatus("test-model");
|
|
76
|
+
const expectation = expect(promise).resolves.toMatchObject({
|
|
77
|
+
status: "loaded",
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
manager.subscribeToProgress("test-model", () => {});
|
|
81
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
82
|
+
|
|
83
|
+
// Emit a terminal status through the shared client's dispatch path
|
|
84
|
+
const client = (manager as any).sseClient;
|
|
85
|
+
const handler = client.subscribers.get("test-model");
|
|
86
|
+
handler({
|
|
87
|
+
event: "status_change",
|
|
88
|
+
model: "test-model",
|
|
89
|
+
data: { status: "loaded" },
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
await expectation;
|
|
93
|
+
} finally {
|
|
94
|
+
vi.unstubAllGlobals();
|
|
95
|
+
vi.useRealTimers();
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
});
|
package/src/interfaces/auth.ts
DELETED
package/src/resolver.ts
DELETED
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import { ApiKeyCredential, ModelThinkingLevel } from "@earendil-works/pi-ai";
|
|
2
|
-
import {
|
|
3
|
-
getAgentDir,
|
|
4
|
-
readStoredCredential,
|
|
5
|
-
SettingsManager,
|
|
6
|
-
} from "@earendil-works/pi-coding-agent";
|
|
7
|
-
import { readFile } from "node:fs/promises";
|
|
8
|
-
import { join } from "node:path";
|
|
9
|
-
import {
|
|
10
|
-
API_KEY_PLACEHOLDER,
|
|
11
|
-
DEFAULT_LLAMA_SERVER_URL,
|
|
12
|
-
DEFAULT_THINKING_BUDGETS,
|
|
13
|
-
} from "./constants";
|
|
14
|
-
|
|
15
|
-
export class ConfigResolver {
|
|
16
|
-
private warnings: string[] = [];
|
|
17
|
-
|
|
18
|
-
private cachedUrls: string[] = [];
|
|
19
|
-
private settingsManager = SettingsManager.create(
|
|
20
|
-
process.cwd(),
|
|
21
|
-
getAgentDir(),
|
|
22
|
-
);
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Resolves the llama-server URL by searching in the global settings.json
|
|
26
|
-
*/
|
|
27
|
-
private async resolveGlobalUrl(): Promise<string | null> {
|
|
28
|
-
const settings = this.settingsManager.getGlobalSettings();
|
|
29
|
-
const { llamaServerUrl = null } = settings as Record<string, string>;
|
|
30
|
-
|
|
31
|
-
return llamaServerUrl;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Resolves the llama-server URL by searching in the project's .pi/settings.json
|
|
36
|
-
*/
|
|
37
|
-
private async resolveProjectUrl(): Promise<string | null> {
|
|
38
|
-
// Warn the user for deprecation
|
|
39
|
-
try {
|
|
40
|
-
const filePath = join(process.cwd(), ".pi", "llama-server.json");
|
|
41
|
-
const { url = null } = JSON.parse(await readFile(filePath, "utf-8"));
|
|
42
|
-
|
|
43
|
-
const messages = [
|
|
44
|
-
"[pi-llama-cpp]",
|
|
45
|
-
"The project-level `.pi/llama-server.json` file has been deprecated.",
|
|
46
|
-
"It will work for now, but you must follow these instructions as soon as possible:",
|
|
47
|
-
'- Move your url to the project-level `.pi/settings.json` file as {"llamaServerUrl": "<url>"}.',
|
|
48
|
-
"- Remove the old `.pi/llama-server.json` file.",
|
|
49
|
-
];
|
|
50
|
-
|
|
51
|
-
this.warnings.push(messages.join("\n"));
|
|
52
|
-
|
|
53
|
-
return url;
|
|
54
|
-
} catch {
|
|
55
|
-
// No old file available, continue as normal
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const settings = this.settingsManager.getProjectSettings();
|
|
59
|
-
const { llamaServerUrl = null } = settings as Record<string, string>;
|
|
60
|
-
|
|
61
|
-
return llamaServerUrl;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Resolves the llama-server URL from the environment
|
|
66
|
-
*/
|
|
67
|
-
private async resolveEnvUrl(): Promise<string | null> {
|
|
68
|
-
return process.env.LLAMA_SERVER_URL ?? null;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Tries all possible ways to retrieve the llama-server URL(s)
|
|
73
|
-
*/
|
|
74
|
-
private async extractJoinedUrls(): Promise<string> {
|
|
75
|
-
// 1. per-project config
|
|
76
|
-
let response = await this.resolveProjectUrl();
|
|
77
|
-
if (response) return response;
|
|
78
|
-
|
|
79
|
-
// 2. env
|
|
80
|
-
response = await this.resolveEnvUrl();
|
|
81
|
-
if (response) return response;
|
|
82
|
-
|
|
83
|
-
// 3. global settings
|
|
84
|
-
response = await this.resolveGlobalUrl();
|
|
85
|
-
if (response) return response;
|
|
86
|
-
|
|
87
|
-
// 4. default
|
|
88
|
-
return DEFAULT_LLAMA_SERVER_URL;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Resolves URLs where llama-servers are running (cached)
|
|
93
|
-
*/
|
|
94
|
-
async resolveUrls(): Promise<string[]> {
|
|
95
|
-
if (this.cachedUrls.length > 0) return this.cachedUrls;
|
|
96
|
-
|
|
97
|
-
const raw = await this.extractJoinedUrls();
|
|
98
|
-
const urls = raw
|
|
99
|
-
.split(";")
|
|
100
|
-
.map((u) => u.trim())
|
|
101
|
-
.filter((u) => u.length > 0)
|
|
102
|
-
.map((u) => u.replace(/\/+$/, ""));
|
|
103
|
-
|
|
104
|
-
this.cachedUrls = urls;
|
|
105
|
-
return this.cachedUrls;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Resolves API key for the provider ID using Pi's stored credentials
|
|
110
|
-
*/
|
|
111
|
-
resolveApiKey(providerId: string): string {
|
|
112
|
-
const credential = readStoredCredential(providerId) as ApiKeyCredential;
|
|
113
|
-
return credential?.key ?? API_KEY_PLACEHOLDER;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Returns warnings collected during URL resolution.
|
|
118
|
-
*/
|
|
119
|
-
getWarnings(): string[] {
|
|
120
|
-
const warnings = [...this.warnings];
|
|
121
|
-
this.warnings.length = 0;
|
|
122
|
-
|
|
123
|
-
return warnings;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/*
|
|
127
|
-
* Resolves the current thinking level from Pi.
|
|
128
|
-
*
|
|
129
|
-
* @returns Selected level
|
|
130
|
-
*/
|
|
131
|
-
resolveThinkingLevel(): ModelThinkingLevel | undefined {
|
|
132
|
-
return this.settingsManager.getDefaultThinkingLevel();
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Resolves the effective thinking budgets from settings
|
|
137
|
-
*
|
|
138
|
-
* @returns Thinking budgets
|
|
139
|
-
*/
|
|
140
|
-
resolveThinkingBudgets(): Record<ModelThinkingLevel, number> {
|
|
141
|
-
const settingsBudgets = this.settingsManager.getThinkingBudgets() ?? {};
|
|
142
|
-
const availableBudgets = {
|
|
143
|
-
...DEFAULT_THINKING_BUDGETS,
|
|
144
|
-
...settingsBudgets,
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
return availableBudgets;
|
|
148
|
-
}
|
|
149
|
-
}
|
package/src/utils/cache.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Generic TTL cache.
|
|
3
|
-
* Entries expire after `ttl` milliseconds from the time they were set.
|
|
4
|
-
*/
|
|
5
|
-
export class Cache {
|
|
6
|
-
private entries = new Map<string, { data: unknown; timestamp: number }>();
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* @param ttl Time-to-live in milliseconds
|
|
10
|
-
*/
|
|
11
|
-
constructor(private readonly ttl: number) {}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Gets a cached value by key. Returns `undefined` if missing or expired.
|
|
15
|
-
*/
|
|
16
|
-
get<T>(key: string): T | undefined {
|
|
17
|
-
const entry = this.entries.get(key);
|
|
18
|
-
if (!entry) return undefined;
|
|
19
|
-
if (Date.now() - entry.timestamp > this.ttl) {
|
|
20
|
-
this.entries.delete(key);
|
|
21
|
-
return undefined;
|
|
22
|
-
}
|
|
23
|
-
return entry.data as T;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Stores a value in the cache with the current timestamp.
|
|
28
|
-
*/
|
|
29
|
-
set(key: string, data: unknown): void {
|
|
30
|
-
this.entries.set(key, { data, timestamp: Date.now() });
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Clears all cached entries.
|
|
35
|
-
*/
|
|
36
|
-
clear(): void {
|
|
37
|
-
this.entries.clear();
|
|
38
|
-
}
|
|
39
|
-
}
|
package/src/utils/mutex.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Ensures only one in-flight operation exists per key.
|
|
3
|
-
* Concurrent callers for the same key share the same promise.
|
|
4
|
-
*/
|
|
5
|
-
export class Mutex {
|
|
6
|
-
private promises = new Map<string, Promise<unknown>>();
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Runs `fn` for the given key, or returns an existing in-flight promise.
|
|
10
|
-
* Concurrent callers for the same key share the same promise.
|
|
11
|
-
*/
|
|
12
|
-
getOrCreate<T>(key: string, fn: () => Promise<T>): Promise<T> {
|
|
13
|
-
const existing = this.promises.get(key);
|
|
14
|
-
if (existing) {
|
|
15
|
-
return existing as Promise<T>;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const promise = fn().finally(() => {
|
|
19
|
-
this.promises.delete(key);
|
|
20
|
-
});
|
|
21
|
-
this.promises.set(key, promise);
|
|
22
|
-
return promise;
|
|
23
|
-
}
|
|
24
|
-
}
|