pi-llama-cpp 0.14.0 → 0.16.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 +52 -13
- package/package.json +3 -3
- package/src/api/client.ts +59 -22
- package/src/constants.ts +5 -0
- package/src/enums/mode.ts +1 -0
- package/src/index.ts +2 -1
- package/src/interfaces/endpoints/models.ts +6 -0
- package/src/interfaces/endpoints/props.ts +14 -0
- package/src/interfaces/settings.ts +6 -0
- package/src/managers/command/models.ts +12 -6
- package/src/managers/command.ts +6 -1
- package/src/managers/server.ts +30 -38
- package/src/managers/settings.ts +23 -3
- package/src/models/legacyModel.ts +3 -3
- package/src/models/llamaSwapModel.ts +90 -0
- package/src/models/routerModel.ts +2 -2
- package/src/models/singleModel.ts +2 -2
- package/src/server.ts +42 -15
- package/src/sse/manager.ts +2 -2
- package/src/ui/dialog/confirm.ts +1 -1
- package/src/ui/dialog/input.ts +1 -1
- package/src/ui/editors/editorOptions.ts +8 -1
- package/src/ui/editors/itemBuilder.ts +2 -6
- package/src/ui/editors/listEditor.ts +24 -15
- package/src/ui/editors/override/entryEditor.ts +105 -23
- package/src/ui/editors/override/fields/base.ts +53 -0
- package/src/ui/editors/override/fields/capabilities.ts +36 -0
- package/src/ui/editors/override/fields/cost.ts +64 -0
- package/src/ui/editors/override/fields/index.ts +68 -0
- package/src/ui/editors/override/fields/numeric.ts +55 -0
- package/src/ui/editors/override/fields/pattern.ts +24 -0
- package/src/ui/editors/override/fields/reasoning.ts +33 -0
- package/src/ui/editors/override/itemBuilder.ts +4 -4
- package/src/ui/editors/server/fields.ts +2 -2
- package/src/ui/editors/server/itemBuilder.ts +2 -3
- package/src/ui/editors/server/serverEditor.ts +58 -22
- package/src/ui/editors/server/utils.ts +50 -0
- package/src/ui/settings/index.ts +11 -0
- package/src/ui/strings.ts +17 -0
- package/src/utils/credentialResolver.ts +110 -0
- package/src/utils/health.ts +2 -2
- package/src/utils/urlResolver.ts +1 -1
- package/tests/{commandManager.test.ts → command/commandManager.test.ts} +80 -9
- package/tests/{events.test.ts → events/events.test.ts} +6 -6
- package/tests/mocks.ts +4 -0
- package/tests/{legacyModel.test.ts → models/legacyModel.test.ts} +5 -5
- package/tests/models/llamaSwapModel.test.ts +327 -0
- package/tests/{routerModel.test.ts → models/routerModel.test.ts} +5 -5
- package/tests/{singleModel.test.ts → models/singleModel.test.ts} +5 -5
- package/tests/{health.test.ts → server/health.test.ts} +4 -4
- package/tests/{server.test.ts → server/server.test.ts} +57 -7
- package/tests/{serverManager.test.ts → server/serverManager.test.ts} +4 -4
- package/tests/{settings.test.ts → settings/settings.test.ts} +198 -355
- package/tests/{settingsStore.test.ts → settings/settingsStore.test.ts} +1 -1
- package/tests/{sseManager.test.ts → sse/sseManager.test.ts} +2 -2
- package/tests/{dialog.test.ts → ui/dialog.test.ts} +5 -4
- package/tests/{overrides.test.ts → ui/overrides.test.ts} +9 -42
- package/tests/utils/credentialResolver.test.ts +66 -0
- package/tests/utils/urlResolver.test.ts +116 -0
- package/src/ui/editors/override/fields.ts +0 -294
- package/src/ui/editors/override/handlers.ts +0 -118
- package/src/ui/editors/server/handlers.ts +0 -32
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { SettingsStore } from "
|
|
2
|
+
import { SettingsStore } from "../../src/utils/settingsStore";
|
|
3
3
|
|
|
4
4
|
const mockReadFile = vi.hoisted(() => vi.fn());
|
|
5
5
|
const mockWriteFile = vi.hoisted(() => vi.fn());
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { SSEManager } from "
|
|
3
|
-
import { createMockServer } from "
|
|
2
|
+
import { SSEManager } from "../../src/sse/manager";
|
|
3
|
+
import { createMockServer } from "../mocks";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Builds an SSEManager bound to a stub server, mirroring how
|
|
@@ -5,10 +5,10 @@ import {
|
|
|
5
5
|
type TUI,
|
|
6
6
|
} from "@earendil-works/pi-tui";
|
|
7
7
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
8
|
-
import { ConfirmDialog } from "
|
|
9
|
-
import { InputDialog } from "
|
|
10
|
-
import { OverrideSettingsList } from "
|
|
11
|
-
import { ServerSettingsList } from "
|
|
8
|
+
import { ConfirmDialog } from "../../src/ui/dialog/confirm";
|
|
9
|
+
import { InputDialog } from "../../src/ui/dialog/input";
|
|
10
|
+
import { OverrideSettingsList } from "../../src/ui/editors/override/overrideList";
|
|
11
|
+
import { ServerSettingsList } from "../../src/ui/editors/server/serverEditor";
|
|
12
12
|
|
|
13
13
|
beforeEach(() => {
|
|
14
14
|
initTheme();
|
|
@@ -139,6 +139,7 @@ describe("ServerSettingsList add wizard", () => {
|
|
|
139
139
|
persist,
|
|
140
140
|
done,
|
|
141
141
|
onError,
|
|
142
|
+
authResolver: () => Promise.resolve("sk-placeholder"),
|
|
142
143
|
});
|
|
143
144
|
return { editor, persist, done, onError };
|
|
144
145
|
};
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it } from "vitest";
|
|
2
|
-
import { FALLBACK_CTX } from "
|
|
3
|
-
import type {
|
|
4
|
-
import { SingleModel } from "
|
|
5
|
-
import { Server } from "
|
|
6
|
-
import { OverrideEntry } from "
|
|
2
|
+
import { FALLBACK_CTX } from "../../src/constants";
|
|
3
|
+
import type { ModelOverride } from "../../src/interfaces/settings";
|
|
4
|
+
import { SingleModel } from "../../src/models/singleModel";
|
|
5
|
+
import { Server } from "../../src/server";
|
|
6
|
+
import { OverrideEntry } from "../../src/ui/editors/override/entry";
|
|
7
7
|
import {
|
|
8
8
|
OverrideFields,
|
|
9
9
|
OverrideSummary,
|
|
10
|
-
} from "
|
|
11
|
-
import {
|
|
12
|
-
import { createMockServer, mockRpc } from "./mocks";
|
|
10
|
+
} from "../../src/ui/editors/override/fields";
|
|
11
|
+
import { createMockServer, mockRpc } from "../mocks";
|
|
13
12
|
|
|
14
13
|
beforeEach(() => {
|
|
15
14
|
mockRpc.mockReset();
|
|
@@ -293,41 +292,9 @@ describe("OverrideSummary.of", () => {
|
|
|
293
292
|
});
|
|
294
293
|
});
|
|
295
294
|
|
|
296
|
-
// ───
|
|
297
|
-
|
|
298
|
-
describe("override entry list helpers", () => {
|
|
299
|
-
const servers = [
|
|
300
|
-
{ url: "http://a", overrides: { "a-1": { reasoning: false } } },
|
|
301
|
-
{ url: "http://b" },
|
|
302
|
-
] as LlamaServer[];
|
|
303
|
-
|
|
304
|
-
it("addEntry should append a pattern → override entry", () => {
|
|
305
|
-
const next = new OverrideEntryMutator(servers, 0).addEntry("a-2", {
|
|
306
|
-
contextSize: 4096,
|
|
307
|
-
});
|
|
308
|
-
expect(next[0].overrides).toEqual({
|
|
309
|
-
"a-1": { reasoning: false },
|
|
310
|
-
"a-2": { contextSize: 4096 },
|
|
311
|
-
});
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
it("pattern renames should replace in place without reordering", () => {
|
|
315
|
-
const mutator = new OverrideEntryMutator(servers, 0);
|
|
316
|
-
const result = mutator.applyFieldChange(0, "pattern", "a-renamed");
|
|
317
|
-
expect(result).not.toBeNull();
|
|
318
|
-
expect(Object.keys(result!.next[0].overrides!)).toEqual(["a-renamed"]);
|
|
319
|
-
expect(result!.next[0].overrides!["a-renamed"]).toEqual({
|
|
320
|
-
reasoning: false,
|
|
321
|
-
});
|
|
322
|
-
expect(result!.next[1]).toBe(servers[1]);
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
it("removeEntry should drop only the targeted entry", () => {
|
|
326
|
-
const withTwo = new OverrideEntryMutator(servers, 0).addEntry("a-2");
|
|
327
|
-
const next = new OverrideEntryMutator(withTwo, 0).removeEntry(0);
|
|
328
|
-
expect(Object.keys(next[0].overrides!)).toEqual(["a-2"]);
|
|
329
|
-
});
|
|
295
|
+
// ─── CostField tests ──────────────────────────────────────────────────────
|
|
330
296
|
|
|
297
|
+
describe("CostField validation and apply", () => {
|
|
331
298
|
it("CostField.validate should accept blank, non-negative numbers; reject the rest", () => {
|
|
332
299
|
const cost = OverrideFields.byId("cost.input");
|
|
333
300
|
expect(cost.validate("")).toBe("0");
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { CredentialResolver } from "../../src/utils/credentialResolver";
|
|
3
|
+
|
|
4
|
+
describe("CredentialResolver", () => {
|
|
5
|
+
const resolver = new CredentialResolver();
|
|
6
|
+
|
|
7
|
+
describe("resolve", () => {
|
|
8
|
+
it("returns placeholder for empty key", () => {
|
|
9
|
+
expect(resolver.resolve("")).toBe("sk-placeholder");
|
|
10
|
+
expect(resolver.resolve(undefined as any)).toBe("sk-placeholder");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("returns literal key as-is", () => {
|
|
14
|
+
expect(resolver.resolve("sk-abc123")).toBe("sk-abc123");
|
|
15
|
+
expect(resolver.resolve("plain-key-without-dollars")).toBe(
|
|
16
|
+
"plain-key-without-dollars",
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("resolves escape sequences $$", () => {
|
|
21
|
+
expect(resolver.resolve("$$literal")).toBe("$literal");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("resolves escape sequences $!", () => {
|
|
25
|
+
expect(resolver.resolve("$!bang")).toBe("!bang");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("resolves $VAR from credential.env", () => {
|
|
29
|
+
expect(resolver.resolve("$MY_KEY", { MY_KEY: "from-env" })).toBe(
|
|
30
|
+
"from-env",
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("resolves ${VAR} from credential.env", () => {
|
|
35
|
+
expect(resolver.resolve("${MY_KEY}", { MY_KEY: "from-env" })).toBe(
|
|
36
|
+
"from-env",
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("resolves $VAR from process.env as fallback", () => {
|
|
41
|
+
process.env.TEST_PROCESS_VAR = "process-value";
|
|
42
|
+
expect(resolver.resolve("$TEST_PROCESS_VAR")).toBe("process-value");
|
|
43
|
+
delete process.env.TEST_PROCESS_VAR;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("prioritizes credential.env over process.env", () => {
|
|
47
|
+
process.env.PREF_KEY = "process-value";
|
|
48
|
+
expect(
|
|
49
|
+
resolver.resolve("$PREF_KEY", { PREF_KEY: "credential-value" }),
|
|
50
|
+
).toBe("credential-value");
|
|
51
|
+
delete process.env.PREF_KEY;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("returns placeholder when env var is missing", () => {
|
|
55
|
+
expect(resolver.resolve("$UNSET_VAR")).toBe("sk-placeholder");
|
|
56
|
+
expect(resolver.resolve("${UNSET_VAR}", { OTHER_KEY: "value" })).toBe(
|
|
57
|
+
"sk-placeholder",
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("returns placeholder for invalid env var syntax", () => {
|
|
62
|
+
expect(resolver.resolve("$123invalid")).toBe("sk-placeholder");
|
|
63
|
+
expect(resolver.resolve("$has-dash")).toBe("sk-placeholder");
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { UrlResolver } from "../../src/utils/urlResolver";
|
|
3
|
+
|
|
4
|
+
function createResolver(
|
|
5
|
+
getLlamaSettings: () => Promise<Record<string, unknown>>,
|
|
6
|
+
getMergedSettings: () => Promise<Record<string, unknown>>,
|
|
7
|
+
) {
|
|
8
|
+
return new UrlResolver({ getLlamaSettings, getMergedSettings });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe("UrlResolver", () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
delete process.env.LLAMA_SERVER_URL;
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe("resolveUrls", () => {
|
|
17
|
+
it("falls back to default when nothing is configured", async () => {
|
|
18
|
+
const resolver = createResolver(
|
|
19
|
+
() => Promise.resolve({}),
|
|
20
|
+
() => Promise.resolve({}),
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const result = await resolver.resolveUrls();
|
|
24
|
+
expect(result).toEqual(["http://127.0.0.1:8080"]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("prioritizes env variable over all config sources", async () => {
|
|
28
|
+
process.env.LLAMA_SERVER_URL = "http://env:9090";
|
|
29
|
+
const resolver = createResolver(
|
|
30
|
+
() =>
|
|
31
|
+
Promise.resolve({
|
|
32
|
+
servers: [{ url: "http://servers:8080" }],
|
|
33
|
+
}),
|
|
34
|
+
() => Promise.resolve({ llamaServerUrl: "http://legacy:7070" }),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const result = await resolver.resolveUrls();
|
|
38
|
+
expect(result).toEqual(["http://env:9090"]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("prioritizes llamaSettings.servers over legacy llamaServerUrl", async () => {
|
|
42
|
+
const resolver = createResolver(
|
|
43
|
+
() =>
|
|
44
|
+
Promise.resolve({
|
|
45
|
+
servers: [{ url: "http://servers:8080" }],
|
|
46
|
+
}),
|
|
47
|
+
() => Promise.resolve({ llamaServerUrl: "http://legacy:7070" }),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const result = await resolver.resolveUrls();
|
|
51
|
+
expect(result).toEqual(["http://servers:8080"]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("falls back to legacy when servers is empty", async () => {
|
|
55
|
+
const resolver = createResolver(
|
|
56
|
+
() => Promise.resolve({ servers: [] }),
|
|
57
|
+
() => Promise.resolve({ llamaServerUrl: "http://legacy:7070" }),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const result = await resolver.resolveUrls();
|
|
61
|
+
expect(result).toEqual(["http://legacy:7070"]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("returns empty arrays from settings when values are falsy", async () => {
|
|
65
|
+
const resolver = createResolver(
|
|
66
|
+
() => Promise.resolve({ servers: undefined }),
|
|
67
|
+
() => Promise.resolve({ llamaServerUrl: undefined }),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const result = await resolver.resolveUrls();
|
|
71
|
+
expect(result).toEqual(["http://127.0.0.1:8080"]);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("warnings", () => {
|
|
76
|
+
it("collects warnings for invalid env URLs", async () => {
|
|
77
|
+
process.env.LLAMA_SERVER_URL = "invalid-url;http://good:8080";
|
|
78
|
+
const resolver = createResolver(
|
|
79
|
+
() => Promise.resolve({}),
|
|
80
|
+
() => Promise.resolve({}),
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
await resolver.resolveUrls();
|
|
84
|
+
expect(resolver.takeWarnings()).toEqual([
|
|
85
|
+
"Ignoring invalid server URL 'invalid-url' (needs http(s)://)",
|
|
86
|
+
]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("collects warnings for invalid server URLs", async () => {
|
|
90
|
+
const resolver = createResolver(
|
|
91
|
+
() =>
|
|
92
|
+
Promise.resolve({
|
|
93
|
+
servers: [{ url: "bad-url" }, { url: "http://good:8080" }],
|
|
94
|
+
}),
|
|
95
|
+
() => Promise.resolve({}),
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
await resolver.resolveUrls();
|
|
99
|
+
expect(resolver.takeWarnings()).toEqual([
|
|
100
|
+
"Ignoring invalid server URL 'bad-url' (needs http(s)://)",
|
|
101
|
+
]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("clears warnings after takeWarnings()", async () => {
|
|
105
|
+
process.env.LLAMA_SERVER_URL = "bad;http://good:8080";
|
|
106
|
+
const resolver = createResolver(
|
|
107
|
+
() => Promise.resolve({}),
|
|
108
|
+
() => Promise.resolve({}),
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
await resolver.resolveUrls();
|
|
112
|
+
expect(resolver.takeWarnings()).toHaveLength(1);
|
|
113
|
+
expect(resolver.takeWarnings()).toEqual([]);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -1,294 +0,0 @@
|
|
|
1
|
-
import type { ModelCost, ModelCostRates } from "@earendil-works/pi-ai";
|
|
2
|
-
import type { ModelOverride } from "../../../interfaces/settings";
|
|
3
|
-
import { FIELDS, type Field } from "../../strings";
|
|
4
|
-
import { SettingField } from "../settingField";
|
|
5
|
-
import { OverrideEntry } from "./entry";
|
|
6
|
-
|
|
7
|
-
/** Type of a field: "input" for free-text, "finite" for fixed options */
|
|
8
|
-
export type FieldType = "input" | "finite";
|
|
9
|
-
|
|
10
|
-
/** Validation function for input fields */
|
|
11
|
-
export type InputValidator = (raw: string) => string | null;
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Abstract base class for one editable field of a model override entry
|
|
15
|
-
* (Template Method): each subclass owns its display format, validation
|
|
16
|
-
* and apply logic in a single place, so the parse/format round-trips
|
|
17
|
-
* can't drift. Extends the shared {@link SettingField} base (id, label,
|
|
18
|
-
* description, placeholder from the FIELDS registry).
|
|
19
|
-
*/
|
|
20
|
-
export abstract class OverrideField extends SettingField<OverrideEntry> {
|
|
21
|
-
/** Field type: "input" for free-text, "finite" for fixed options. */
|
|
22
|
-
abstract readonly type: FieldType;
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Canonical display label for this field — the single source of truth
|
|
26
|
-
* for both the field rows' `currentValue` and the post-commit refresh
|
|
27
|
-
* of an open field submenu, so the two can't drift.
|
|
28
|
-
*/
|
|
29
|
-
abstract displayValue(entry: OverrideEntry): string;
|
|
30
|
-
|
|
31
|
-
/** Current value shown in the row — same rendering as the post-commit
|
|
32
|
-
* refresh of an open field submenu, so the two can't drift. */
|
|
33
|
-
currentValue(entry: OverrideEntry): string {
|
|
34
|
-
return this.displayValue(entry);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** For "input" fields: validation function for committed values. */
|
|
38
|
-
abstract readonly validate: InputValidator;
|
|
39
|
-
|
|
40
|
-
/** For "finite" fields: the fixed options (undefined for "input"). */
|
|
41
|
-
readonly options: readonly string[] | undefined = undefined;
|
|
42
|
-
|
|
43
|
-
/** Applies a committed value to the override object. The pattern key
|
|
44
|
-
* rename is handled separately by the mutator. */
|
|
45
|
-
abstract apply(override: ModelOverride, value: string): void;
|
|
46
|
-
|
|
47
|
-
/** One `term: value` part of the entry-row summary (see
|
|
48
|
-
* {@link OverrideSummary.of}), or `null` when the field shouldn't
|
|
49
|
-
* appear (unset/zero values — each field decides). The pattern is the
|
|
50
|
-
* row label, so it never contributes a part; the default is `null`. */
|
|
51
|
-
summaryPart(_override: ModelOverride): string | null {
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** Parses a non-negative number; blank input means zero (unset fields
|
|
56
|
-
* default to zero at read time). Returns `null` when invalid. Shared
|
|
57
|
-
* by the cost and numeric fields so their validation can't drift. */
|
|
58
|
-
protected static parseNonNegative(raw: string): number | null {
|
|
59
|
-
const n = Number(raw.trim());
|
|
60
|
-
return isFinite(n) && n >= 0 ? n : null;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* The entry's pattern key. The rename itself is handled by the mutator
|
|
66
|
-
* (`OverrideEntryMutator`); {@link apply} is a no-op.
|
|
67
|
-
*/
|
|
68
|
-
class PatternField extends OverrideField {
|
|
69
|
-
readonly id = "pattern";
|
|
70
|
-
readonly type = "input";
|
|
71
|
-
readonly field = FIELDS.pattern;
|
|
72
|
-
|
|
73
|
-
displayValue(entry: OverrideEntry): string {
|
|
74
|
-
return entry.pattern;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
readonly validate: InputValidator = (raw) => {
|
|
78
|
-
const trimmed = raw.trim();
|
|
79
|
-
return trimmed.length > 0 ? trimmed : null;
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
apply(): void {
|
|
83
|
-
// The pattern IS the map key — renames go through the mutator.
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* One `ModelCostRates` key of the override's `cost` object (absorbs the
|
|
89
|
-
* former `CostFieldDefFactory` and the standalone `parseCostValue` /
|
|
90
|
-
* `applyCostFieldValue` helpers): owns the cost parse and immutable
|
|
91
|
-
* apply in the same place as its display format.
|
|
92
|
-
*/
|
|
93
|
-
class CostField extends OverrideField {
|
|
94
|
-
readonly type = "input";
|
|
95
|
-
override readonly field: Field;
|
|
96
|
-
|
|
97
|
-
constructor(
|
|
98
|
-
readonly id: string,
|
|
99
|
-
private readonly key: keyof ModelCostRates,
|
|
100
|
-
field: Field,
|
|
101
|
-
/** Short term used in the entry-row summary (e.g. `cache read`). */
|
|
102
|
-
private readonly summaryTerm: string,
|
|
103
|
-
) {
|
|
104
|
-
super();
|
|
105
|
-
this.field = field;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
displayValue(entry: OverrideEntry): string {
|
|
109
|
-
return String(entry.override.cost?.[this.key] ?? 0);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
readonly validate: InputValidator = (raw) => {
|
|
113
|
-
const parsed = OverrideField.parseNonNegative(raw);
|
|
114
|
-
return parsed === null ? null : String(parsed);
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
apply(override: ModelOverride, value: string): void {
|
|
118
|
-
const nextCost = this.applyCost(override.cost, Number(value));
|
|
119
|
-
if (nextCost) override.cost = nextCost;
|
|
120
|
-
else delete override.cost;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** Returns a copy of `cost` with this field set to `value`. A `0` (or
|
|
124
|
-
* non-finite) value removes the field — unset cost fields default to
|
|
125
|
-
* zero at read time — and when no fields remain, returns `undefined`
|
|
126
|
-
* so the whole `cost` object can be dropped. Immutable. */
|
|
127
|
-
private applyCost(
|
|
128
|
-
cost: ModelOverride["cost"],
|
|
129
|
-
value: number,
|
|
130
|
-
): Partial<ModelCost> | undefined {
|
|
131
|
-
const next = { ...cost };
|
|
132
|
-
if (isFinite(value) && value > 0) next[this.key] = value;
|
|
133
|
-
else delete next[this.key];
|
|
134
|
-
return Object.keys(next).length > 0 ? next : undefined;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** Appears in the summary only when a non-zero cost is set. */
|
|
138
|
-
override summaryPart(override: ModelOverride): string | null {
|
|
139
|
-
const value = override.cost?.[this.key];
|
|
140
|
-
return value ? `${this.summaryTerm}: $${value}` : null;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Non-negative numeric field (`contextSize`, `maxTokens`): keeps the
|
|
146
|
-
* display format, validator and apply logic in a single place so the
|
|
147
|
-
* three can't drift (e.g. the validator accepting `0` while the apply
|
|
148
|
-
* drops it — unset fields default to zero at read time, so all render
|
|
149
|
-
* as `0`).
|
|
150
|
-
*/
|
|
151
|
-
class NumericField extends OverrideField {
|
|
152
|
-
readonly type = "input";
|
|
153
|
-
override readonly field: Field;
|
|
154
|
-
|
|
155
|
-
constructor(
|
|
156
|
-
readonly id: "maxTokens" | "contextSize",
|
|
157
|
-
field: Field,
|
|
158
|
-
) {
|
|
159
|
-
super();
|
|
160
|
-
this.field = field;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
displayValue(entry: OverrideEntry): string {
|
|
164
|
-
return String(entry.override[this.id] ?? 0);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/** Accepts finite, non-negative numbers (blank means zero). */
|
|
168
|
-
readonly validate: InputValidator = (raw) => {
|
|
169
|
-
const n = OverrideField.parseNonNegative(raw);
|
|
170
|
-
return n === null ? null : String(n);
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
/** Applies the committed value; non-positive values remove the key. */
|
|
174
|
-
apply(override: ModelOverride, value: string): void {
|
|
175
|
-
const n = Number(value);
|
|
176
|
-
if (isFinite(n) && n > 0) override[this.id] = n;
|
|
177
|
-
else delete override[this.id];
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/** Appears in the summary only when set. */
|
|
181
|
-
override summaryPart(override: ModelOverride): string | null {
|
|
182
|
-
const value = override[this.id];
|
|
183
|
-
return value === undefined ? null : `${this.id}: ${value}`;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/** The override's `capabilities` list (finite: fixed label options). */
|
|
188
|
-
class CapabilitiesField extends OverrideField {
|
|
189
|
-
readonly id = "capabilities";
|
|
190
|
-
readonly type = "finite";
|
|
191
|
-
readonly field = FIELDS.capabilities;
|
|
192
|
-
override readonly options = ["text", "text | image"];
|
|
193
|
-
|
|
194
|
-
displayValue(entry: OverrideEntry): string {
|
|
195
|
-
return entry.override.capabilities?.join(" | ") ?? "text";
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
readonly validate: InputValidator = (raw) =>
|
|
199
|
-
this.options?.includes(raw) ? raw : null;
|
|
200
|
-
|
|
201
|
-
apply(override: ModelOverride, value: string): void {
|
|
202
|
-
override.capabilities =
|
|
203
|
-
value === "text | image"
|
|
204
|
-
? ["text", "image"]
|
|
205
|
-
: value === "text"
|
|
206
|
-
? ["text"]
|
|
207
|
-
: undefined;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/** Appears in the summary only when non-empty. */
|
|
211
|
-
override summaryPart(override: ModelOverride): string | null {
|
|
212
|
-
return override.capabilities?.length
|
|
213
|
-
? `capabilities: ${override.capabilities.join(",")}`
|
|
214
|
-
: null;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/** The override's `reasoning` flag (finite: true/false, default true). */
|
|
219
|
-
class ReasoningField extends OverrideField {
|
|
220
|
-
readonly id = "reasoning";
|
|
221
|
-
readonly type = "finite";
|
|
222
|
-
readonly field = FIELDS.reasoning;
|
|
223
|
-
override readonly options = ["true", "false"];
|
|
224
|
-
|
|
225
|
-
displayValue(entry: OverrideEntry): string {
|
|
226
|
-
return entry.override.reasoning === false ? "false" : "true";
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
readonly validate: InputValidator = (raw) =>
|
|
230
|
-
this.options?.includes(raw) ? raw : null;
|
|
231
|
-
|
|
232
|
-
apply(override: ModelOverride, value: string): void {
|
|
233
|
-
if (value === "true") override.reasoning = true;
|
|
234
|
-
else if (value === "false") override.reasoning = false;
|
|
235
|
-
else delete override.reasoning;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/** Appears in the summary only when explicitly set. */
|
|
239
|
-
override summaryPart(override: ModelOverride): string | null {
|
|
240
|
-
return override.reasoning === undefined
|
|
241
|
-
? null
|
|
242
|
-
: `reasoning: ${override.reasoning}`;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/** Cost field specs: [ModelCostRates key, FIELDS entry, summary term] */
|
|
247
|
-
const COST_FIELDS: [keyof ModelCostRates, Field, string][] = [
|
|
248
|
-
["input", FIELDS.inputCost, "input"],
|
|
249
|
-
["output", FIELDS.outputCost, "output"],
|
|
250
|
-
["cacheRead", FIELDS.cacheReadCost, "cache read"],
|
|
251
|
-
["cacheWrite", FIELDS.cacheWriteCost, "cache write"],
|
|
252
|
-
];
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* The field registry for model override entries: the complete list of
|
|
256
|
-
* editable fields (order defines the row order in the settings list)
|
|
257
|
-
* plus id-based lookup. Throws on an unknown id — the registry is
|
|
258
|
-
* static, so a miss is a programming error.
|
|
259
|
-
*/
|
|
260
|
-
export class OverrideFields {
|
|
261
|
-
static readonly all: OverrideField[] = [
|
|
262
|
-
new PatternField(),
|
|
263
|
-
...COST_FIELDS.map(
|
|
264
|
-
([key, field, term]) => new CostField(`cost.${key}`, key, field, term),
|
|
265
|
-
),
|
|
266
|
-
new CapabilitiesField(),
|
|
267
|
-
new ReasoningField(),
|
|
268
|
-
new NumericField("contextSize", FIELDS.contextSize),
|
|
269
|
-
new NumericField("maxTokens", FIELDS.maxTokens),
|
|
270
|
-
];
|
|
271
|
-
|
|
272
|
-
static byId(id: string): OverrideField {
|
|
273
|
-
const field = this.all.find((f) => f.id === id);
|
|
274
|
-
if (!field) throw new Error(`Unknown override field: ${id}`);
|
|
275
|
-
return field;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* Builds the entry-row summary shown in the override entry list
|
|
281
|
-
* (`input: $0.2, output: $0.6, cache read: $0.01, …`), composed from the
|
|
282
|
-
* {@link OverrideFields} registry — adding a field to the registry makes
|
|
283
|
-
* it show up here too, so the summary can't lag the edit menu. Fields
|
|
284
|
-
* with unset/zero values are omitted for brevity (each field decides via
|
|
285
|
-
* {@link OverrideField.summaryPart}); `—` when nothing is set.
|
|
286
|
-
*/
|
|
287
|
-
export class OverrideSummary {
|
|
288
|
-
static of(override: ModelOverride): string {
|
|
289
|
-
const parts = OverrideFields.all
|
|
290
|
-
.map((field) => field.summaryPart(override))
|
|
291
|
-
.filter((part): part is string => part !== null);
|
|
292
|
-
return parts.length > 0 ? parts.join(", ") : "—";
|
|
293
|
-
}
|
|
294
|
-
}
|