pi-llama-cpp 0.14.0 → 0.15.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 +12 -10
- package/package.json +3 -3
- package/src/constants.ts +5 -0
- package/src/index.ts +2 -1
- package/src/interfaces/settings.ts +6 -0
- package/src/managers/command/models.ts +9 -2
- package/src/managers/command.ts +6 -1
- package/src/managers/server.ts +30 -38
- package/src/managers/settings.ts +14 -2
- package/src/server.ts +6 -9
- package/src/sse/manager.ts +2 -2
- 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 +97 -15
- 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 +3 -6
- package/src/ui/editors/server/itemBuilder.ts +1 -5
- package/src/ui/editors/server/serverEditor.ts +51 -15
- 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/health.ts +2 -1
- 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 +2 -0
- package/tests/{legacyModel.test.ts → models/legacyModel.test.ts} +5 -5
- 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} +2 -2
- package/tests/{server.test.ts → server/server.test.ts} +5 -5
- package/tests/{serverManager.test.ts → server/serverManager.test.ts} +4 -4
- package/tests/{settings.test.ts → settings/settings.test.ts} +174 -133
- 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/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,11 +1,11 @@
|
|
|
1
1
|
import type { SettingItem, SettingsList } from "@earendil-works/pi-tui";
|
|
2
|
+
import type { LlamaServer, ModelOverride } from "../../../interfaces/settings";
|
|
2
3
|
import { FIELDS, FieldMessages, HINTS, TITLES } from "../../strings";
|
|
3
4
|
import type { OverrideSettingsListOptions } from "../editorOptions";
|
|
4
5
|
import { ListEditor } from "../listEditor";
|
|
5
6
|
import { SettingsListFactory } from "../settingsListFactory";
|
|
6
7
|
import { OverrideEntry } from "./entry";
|
|
7
|
-
import { OverrideFields, OverrideSummary } from "./fields";
|
|
8
|
-
import { OverrideEntryMutator } from "./handlers";
|
|
8
|
+
import { OverrideFields, OverrideSummary } from "./fields/index";
|
|
9
9
|
import { OverrideItemBuilder } from "./itemBuilder";
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -39,13 +39,13 @@ export class OverrideEntryListEditor extends ListEditor<OverrideSettingsListOpti
|
|
|
39
39
|
|
|
40
40
|
protected buildSettingsList(): SettingsList {
|
|
41
41
|
const builder = new OverrideItemBuilder(this.dialogs);
|
|
42
|
-
const items: SettingItem[] = this.
|
|
42
|
+
const items: SettingItem[] = this.entries().map((entry, i) => ({
|
|
43
43
|
id: `entry-${i}`,
|
|
44
44
|
label: entry.pattern,
|
|
45
45
|
description: HINTS.overrideEntryRow,
|
|
46
46
|
currentValue: OverrideSummary.of(entry.override),
|
|
47
47
|
submenu: (_cv, done) => {
|
|
48
|
-
const current = this.
|
|
48
|
+
const current = this.entries()[i] ?? entry;
|
|
49
49
|
const fieldItems = builder.buildFieldItems(current);
|
|
50
50
|
const fieldList = SettingsListFactory.create(
|
|
51
51
|
fieldItems,
|
|
@@ -75,7 +75,7 @@ export class OverrideEntryListEditor extends ListEditor<OverrideSettingsListOpti
|
|
|
75
75
|
field: string,
|
|
76
76
|
value: string,
|
|
77
77
|
): void {
|
|
78
|
-
const result = this.
|
|
78
|
+
const result = this.applyFieldChange(entryIndex, field, value);
|
|
79
79
|
if (!result) return;
|
|
80
80
|
|
|
81
81
|
this.commitFieldChange(
|
|
@@ -121,7 +121,7 @@ export class OverrideEntryListEditor extends ListEditor<OverrideSettingsListOpti
|
|
|
121
121
|
|
|
122
122
|
protected deleteSelected(): void {
|
|
123
123
|
const idx = this.selectedIndex;
|
|
124
|
-
const next = this.
|
|
124
|
+
const next = this.removeEntry(idx);
|
|
125
125
|
void this.persistSnapshot(next, () => {
|
|
126
126
|
this.rebuildList(idx);
|
|
127
127
|
this.options.onChanged?.();
|
|
@@ -131,8 +131,8 @@ export class OverrideEntryListEditor extends ListEditor<OverrideSettingsListOpti
|
|
|
131
131
|
|
|
132
132
|
protected readonly emptyHintKey = "emptyOverrideEntries" as const;
|
|
133
133
|
|
|
134
|
-
protected
|
|
135
|
-
return this.
|
|
134
|
+
protected getCount(): number {
|
|
135
|
+
return this.entries().length;
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
protected getRowId(index: number): string {
|
|
@@ -140,7 +140,7 @@ export class OverrideEntryListEditor extends ListEditor<OverrideSettingsListOpti
|
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
protected getRowLabel(index: number): string {
|
|
143
|
-
return this.
|
|
143
|
+
return this.entries()[index]?.pattern ?? "";
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
protected get deleteTitle(): string {
|
|
@@ -149,16 +149,98 @@ export class OverrideEntryListEditor extends ListEditor<OverrideSettingsListOpti
|
|
|
149
149
|
|
|
150
150
|
// -- helpers ---------------------------------------------------------------
|
|
151
151
|
|
|
152
|
-
/**
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
152
|
+
/** The server's override entries, in map iteration order. */
|
|
153
|
+
private entries(): OverrideEntry[] {
|
|
154
|
+
return OverrideEntry.listFrom(this.options.servers[this.serverIndex]);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Handles a field commit from an override entry's submenu. Returns
|
|
158
|
+
* `null` when the entry doesn't exist (stale index — no-op). */
|
|
159
|
+
private applyFieldChange(
|
|
160
|
+
entryIndex: number,
|
|
161
|
+
field: string,
|
|
162
|
+
value: string,
|
|
163
|
+
): { next: LlamaServer[]; updatedOverride: ModelOverride } | null {
|
|
164
|
+
const entry = this.entries()[entryIndex];
|
|
165
|
+
if (!entry) return null;
|
|
166
|
+
|
|
167
|
+
// Pattern field: rename the pattern key, keep the override object
|
|
168
|
+
if (field === "pattern") {
|
|
169
|
+
return {
|
|
170
|
+
next: this.updateEntry(entryIndex, value, entry.override),
|
|
171
|
+
updatedOverride: entry.override,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Other fields: let the field definition apply itself
|
|
176
|
+
const updatedOverride: ModelOverride = { ...entry.override };
|
|
177
|
+
OverrideFields.byId(field).apply(updatedOverride, value);
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
next: this.updateEntry(entryIndex, entry.pattern, updatedOverride),
|
|
181
|
+
updatedOverride,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Adds a new override entry with a uniquified pattern. */
|
|
186
|
+
private addEntry(pattern: string): LlamaServer[] {
|
|
187
|
+
const server = this.options.servers[this.serverIndex];
|
|
188
|
+
if (!server) return this.options.servers;
|
|
189
|
+
|
|
190
|
+
const existing = new Set(Object.keys(server.overrides ?? {}));
|
|
191
|
+
let uniquePattern = pattern;
|
|
192
|
+
let n = 2;
|
|
193
|
+
while (existing.has(uniquePattern)) {
|
|
194
|
+
uniquePattern = `${pattern}-${n++}`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return this.withServer((s) => ({
|
|
198
|
+
...s,
|
|
199
|
+
overrides: { ...s.overrides, [uniquePattern]: {} },
|
|
200
|
+
}));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Removes the entry at `entryIndex`. */
|
|
204
|
+
private removeEntry(entryIndex: number): LlamaServer[] {
|
|
205
|
+
return this.withServer((s) => ({
|
|
206
|
+
...s,
|
|
207
|
+
overrides: Object.fromEntries(
|
|
208
|
+
Object.entries(s.overrides ?? {}).filter((_, j) => j !== entryIndex),
|
|
209
|
+
),
|
|
210
|
+
}));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Returns the servers with the target server replaced by
|
|
214
|
+
* `fn(server)`. Immutable. */
|
|
215
|
+
private withServer(fn: (server: LlamaServer) => LlamaServer): LlamaServer[] {
|
|
216
|
+
return this.options.servers.map((server, i) =>
|
|
217
|
+
i === this.serverIndex ? fn(server) : server,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Replaces the entry at `entryIndex` with `pattern → override` in a
|
|
222
|
+
* single mutation — used both for renaming the pattern and for editing
|
|
223
|
+
* its fields. The entry keeps its position in the map's iteration
|
|
224
|
+
* order. Immutable. */
|
|
225
|
+
private updateEntry(
|
|
226
|
+
entryIndex: number,
|
|
227
|
+
pattern: string,
|
|
228
|
+
override: ModelOverride,
|
|
229
|
+
): LlamaServer[] {
|
|
230
|
+
return this.withServer((s) => ({
|
|
231
|
+
...s,
|
|
232
|
+
overrides: Object.fromEntries(
|
|
233
|
+
Object.entries(s.overrides ?? {}).map(([k, v], j) =>
|
|
234
|
+
j === entryIndex ? [pattern, override] : [k, v],
|
|
235
|
+
),
|
|
236
|
+
),
|
|
237
|
+
}));
|
|
156
238
|
}
|
|
157
239
|
|
|
158
240
|
private async saveAdd(pattern: string): Promise<void> {
|
|
159
|
-
const next = this.
|
|
241
|
+
const next = this.addEntry(pattern);
|
|
160
242
|
await this.persistSnapshot(next, () => {
|
|
161
|
-
this.rebuildList(this.
|
|
243
|
+
this.rebuildList(this.entries().length - 1);
|
|
162
244
|
this.options.onChanged?.();
|
|
163
245
|
this.options.tui.requestRender();
|
|
164
246
|
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ModelOverride } from "../../../../interfaces/settings";
|
|
2
|
+
import { SettingField } from "../../settingField";
|
|
3
|
+
import { OverrideEntry } from "../entry";
|
|
4
|
+
|
|
5
|
+
/** Type of a field: "input" for free-text, "finite" for fixed options */
|
|
6
|
+
export type FieldType = "input" | "finite";
|
|
7
|
+
|
|
8
|
+
/** Validation function for input fields */
|
|
9
|
+
export type InputValidator = (raw: string) => string | null;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Abstract base class for one editable field of a model override entry
|
|
13
|
+
* (Template Method): each subclass owns its display format, validation
|
|
14
|
+
* and apply logic in a single place, so the parse/format round-trips
|
|
15
|
+
* can't drift. Extends the shared {@link SettingField} base (id, label,
|
|
16
|
+
* description, placeholder from the FIELDS registry).
|
|
17
|
+
*/
|
|
18
|
+
export abstract class OverrideField extends SettingField<OverrideEntry> {
|
|
19
|
+
/** Field type: "input" for free-text, "finite" for fixed options. */
|
|
20
|
+
abstract readonly type: FieldType;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Canonical display label for this field — the single source of truth
|
|
24
|
+
* for both the field rows' `currentValue` and the post-commit refresh
|
|
25
|
+
* of an open field submenu, so the two can't drift.
|
|
26
|
+
*/
|
|
27
|
+
abstract displayValue(entry: OverrideEntry): string;
|
|
28
|
+
|
|
29
|
+
/** Current value shown in the row — defaults to `displayValue`. */
|
|
30
|
+
currentValue(entry: OverrideEntry): string {
|
|
31
|
+
return this.displayValue(entry);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** For "input" fields: validation function for committed values. */
|
|
35
|
+
abstract readonly validate: InputValidator;
|
|
36
|
+
|
|
37
|
+
/** For "finite" fields: the fixed options (undefined for "input"). */
|
|
38
|
+
readonly options: readonly string[] | undefined = undefined;
|
|
39
|
+
|
|
40
|
+
/** Applies a committed value to the override object. The pattern key
|
|
41
|
+
* rename is handled separately by the mutator (default: no-op). */
|
|
42
|
+
apply(_override: ModelOverride, _value: string): void {
|
|
43
|
+
// no-op — override in subclasses that need to mutate the override
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** One `term: value` part of the entry-row summary (see
|
|
47
|
+
* {@link OverrideSummary.of}), or `null` when the field shouldn't
|
|
48
|
+
* appear (unset/zero values — each field decides). The pattern is the
|
|
49
|
+
* row label, so it never contributes a part; the default is `null`. */
|
|
50
|
+
summaryPart(_override: ModelOverride): string | null {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ModelOverride } from "../../../../interfaces/settings";
|
|
2
|
+
import { FIELDS } from "../../../strings";
|
|
3
|
+
import { OverrideEntry } from "../entry";
|
|
4
|
+
import type { InputValidator } from "./base";
|
|
5
|
+
import { OverrideField } from "./base";
|
|
6
|
+
|
|
7
|
+
/** The override's `capabilities` list (finite: fixed label options). */
|
|
8
|
+
export class CapabilitiesField extends OverrideField {
|
|
9
|
+
readonly id = "capabilities";
|
|
10
|
+
readonly type = "finite";
|
|
11
|
+
readonly field = FIELDS.capabilities;
|
|
12
|
+
override readonly options = ["text", "text | image"];
|
|
13
|
+
|
|
14
|
+
displayValue(entry: OverrideEntry): string {
|
|
15
|
+
return entry.override.capabilities?.join(" | ") ?? "text";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
readonly validate: InputValidator = (raw) =>
|
|
19
|
+
this.options?.includes(raw) ? raw : null;
|
|
20
|
+
|
|
21
|
+
apply(override: ModelOverride, value: string): void {
|
|
22
|
+
override.capabilities =
|
|
23
|
+
value === "text | image"
|
|
24
|
+
? ["text", "image"]
|
|
25
|
+
: value === "text"
|
|
26
|
+
? ["text"]
|
|
27
|
+
: undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Appears in the summary only when non-empty. */
|
|
31
|
+
override summaryPart(override: ModelOverride): string | null {
|
|
32
|
+
return override.capabilities?.length
|
|
33
|
+
? `capabilities: ${override.capabilities.join(",")}`
|
|
34
|
+
: null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { ModelCost, ModelCostRates } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { ModelOverride } from "../../../../interfaces/settings";
|
|
3
|
+
import type { Field } from "../../../strings";
|
|
4
|
+
import { OverrideEntry } from "../entry";
|
|
5
|
+
import type { InputValidator } from "./base";
|
|
6
|
+
import { OverrideField } from "./base";
|
|
7
|
+
import { parseNonNegative } from "./numeric";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* One `ModelCostRates` key of the override's `cost` object (absorbs the
|
|
11
|
+
* former `CostFieldDefFactory` and the standalone `parseCostValue` /
|
|
12
|
+
* `applyCostFieldValue` helpers): owns the cost parse and immutable
|
|
13
|
+
* apply in the same place as its display format.
|
|
14
|
+
*/
|
|
15
|
+
export class CostField extends OverrideField {
|
|
16
|
+
readonly type = "input";
|
|
17
|
+
override readonly field: Field;
|
|
18
|
+
|
|
19
|
+
constructor(
|
|
20
|
+
readonly id: string,
|
|
21
|
+
private readonly key: keyof ModelCostRates,
|
|
22
|
+
field: Field,
|
|
23
|
+
/** Short term used in the entry-row summary (e.g. `cache read`). */
|
|
24
|
+
private readonly summaryTerm: string,
|
|
25
|
+
) {
|
|
26
|
+
super();
|
|
27
|
+
this.field = field;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
displayValue(entry: OverrideEntry): string {
|
|
31
|
+
return String(entry.override.cost?.[this.key] ?? 0);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
readonly validate: InputValidator = (raw) => {
|
|
35
|
+
const parsed = parseNonNegative(raw);
|
|
36
|
+
return parsed === null ? null : String(parsed);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
apply(override: ModelOverride, value: string): void {
|
|
40
|
+
const nextCost = this.applyCost(override.cost, Number(value));
|
|
41
|
+
if (nextCost) override.cost = nextCost;
|
|
42
|
+
else delete override.cost;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Returns a copy of `cost` with this field set to `value`. A `0` (or
|
|
46
|
+
* non-finite) value removes the field — unset cost fields default to
|
|
47
|
+
* zero at read time — and when no fields remain, returns `undefined`
|
|
48
|
+
* so the whole `cost` object can be dropped. Immutable. */
|
|
49
|
+
private applyCost(
|
|
50
|
+
cost: ModelOverride["cost"],
|
|
51
|
+
value: number,
|
|
52
|
+
): Partial<ModelCost> | undefined {
|
|
53
|
+
const next = { ...cost };
|
|
54
|
+
if (isFinite(value) && value > 0) next[this.key] = value;
|
|
55
|
+
else delete next[this.key];
|
|
56
|
+
return Object.keys(next).length > 0 ? next : undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Appears in the summary only when a non-zero cost is set. */
|
|
60
|
+
override summaryPart(override: ModelOverride): string | null {
|
|
61
|
+
const value = override.cost?.[this.key];
|
|
62
|
+
return value ? `${this.summaryTerm}: $${value}` : null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { ModelCostRates } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { ModelOverride } from "../../../../interfaces/settings";
|
|
3
|
+
import { FIELDS } from "../../../strings";
|
|
4
|
+
import { OverrideField } from "./base";
|
|
5
|
+
import { CapabilitiesField } from "./capabilities";
|
|
6
|
+
import { CostField } from "./cost";
|
|
7
|
+
import { NumericField } from "./numeric";
|
|
8
|
+
import { PatternField } from "./pattern";
|
|
9
|
+
import { ReasoningField } from "./reasoning";
|
|
10
|
+
|
|
11
|
+
/** Cost field specs: [ModelCostRates key, FIELDS entry, summary term] */
|
|
12
|
+
const COST_FIELDS: [
|
|
13
|
+
keyof ModelCostRates,
|
|
14
|
+
(
|
|
15
|
+
| typeof FIELDS.inputCost
|
|
16
|
+
| typeof FIELDS.outputCost
|
|
17
|
+
| typeof FIELDS.cacheReadCost
|
|
18
|
+
| typeof FIELDS.cacheWriteCost
|
|
19
|
+
),
|
|
20
|
+
string,
|
|
21
|
+
][] = [
|
|
22
|
+
["input", FIELDS.inputCost, "input"],
|
|
23
|
+
["output", FIELDS.outputCost, "output"],
|
|
24
|
+
["cacheRead", FIELDS.cacheReadCost, "cache read"],
|
|
25
|
+
["cacheWrite", FIELDS.cacheWriteCost, "cache write"],
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The field registry for model override entries: the complete list of
|
|
30
|
+
* editable fields (order defines the row order in the settings list)
|
|
31
|
+
* plus id-based lookup. Throws on an unknown id — the registry is
|
|
32
|
+
* static, so a miss is a programming error.
|
|
33
|
+
*/
|
|
34
|
+
export class OverrideFields {
|
|
35
|
+
static readonly all: OverrideField[] = [
|
|
36
|
+
new PatternField(),
|
|
37
|
+
...COST_FIELDS.map(
|
|
38
|
+
([key, field, term]) => new CostField(`cost.${key}`, key, field, term),
|
|
39
|
+
),
|
|
40
|
+
new CapabilitiesField(),
|
|
41
|
+
new ReasoningField(),
|
|
42
|
+
new NumericField("contextSize", FIELDS.contextSize),
|
|
43
|
+
new NumericField("maxTokens", FIELDS.maxTokens),
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
static byId(id: string): OverrideField {
|
|
47
|
+
const field = this.all.find((f) => f.id === id);
|
|
48
|
+
if (!field) throw new Error(`Unknown override field: ${id}`);
|
|
49
|
+
return field;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Builds the entry-row summary shown in the override entry list
|
|
55
|
+
* (`input: $0.2, output: $0.6, cache read: $0.01, …`), composed from the
|
|
56
|
+
* {@link OverrideFields} registry — adding a field to the registry makes
|
|
57
|
+
* it show up here too, so the summary can't lag the edit menu. Fields
|
|
58
|
+
* with unset/zero values are omitted for brevity (each field decides via
|
|
59
|
+
* {@link OverrideField.summaryPart}); `—` when nothing is set.
|
|
60
|
+
*/
|
|
61
|
+
export class OverrideSummary {
|
|
62
|
+
static of(override: ModelOverride): string {
|
|
63
|
+
const parts = OverrideFields.all
|
|
64
|
+
.map((field) => field.summaryPart(override))
|
|
65
|
+
.filter((part): part is string => part !== null);
|
|
66
|
+
return parts.length > 0 ? parts.join(", ") : "—";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ModelOverride } from "../../../../interfaces/settings";
|
|
2
|
+
import type { Field } from "../../../strings";
|
|
3
|
+
import { OverrideEntry } from "../entry";
|
|
4
|
+
import type { InputValidator } from "./base";
|
|
5
|
+
import { OverrideField } from "./base";
|
|
6
|
+
|
|
7
|
+
/** Parses a non-negative number; blank input means zero (unset fields
|
|
8
|
+
* default to zero at read time). Returns `null` when invalid. */
|
|
9
|
+
export function parseNonNegative(raw: string): number | null {
|
|
10
|
+
const n = Number(raw.trim());
|
|
11
|
+
return isFinite(n) && n >= 0 ? n : null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Non-negative numeric field (`contextSize`, `maxTokens`): keeps the
|
|
16
|
+
* display format, validator and apply logic in a single place so the
|
|
17
|
+
* three can't drift (e.g. the validator accepting `0` while the apply
|
|
18
|
+
* drops it — unset fields default to zero at read time, so all render
|
|
19
|
+
* as `0`).
|
|
20
|
+
*/
|
|
21
|
+
export class NumericField extends OverrideField {
|
|
22
|
+
readonly type = "input";
|
|
23
|
+
override readonly field: Field;
|
|
24
|
+
|
|
25
|
+
constructor(
|
|
26
|
+
readonly id: "maxTokens" | "contextSize",
|
|
27
|
+
field: Field,
|
|
28
|
+
) {
|
|
29
|
+
super();
|
|
30
|
+
this.field = field;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
displayValue(entry: OverrideEntry): string {
|
|
34
|
+
return String(entry.override[this.id] ?? 0);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Accepts finite, non-negative numbers (blank means zero). */
|
|
38
|
+
readonly validate: InputValidator = (raw) => {
|
|
39
|
+
const n = parseNonNegative(raw);
|
|
40
|
+
return n === null ? null : String(n);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** Applies the committed value; non-positive values remove the key. */
|
|
44
|
+
apply(override: ModelOverride, value: string): void {
|
|
45
|
+
const n = Number(value);
|
|
46
|
+
if (isFinite(n) && n > 0) override[this.id] = n;
|
|
47
|
+
else delete override[this.id];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Appears in the summary only when set. */
|
|
51
|
+
override summaryPart(override: ModelOverride): string | null {
|
|
52
|
+
const value = override[this.id];
|
|
53
|
+
return value === undefined ? null : `${this.id}: ${value}`;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { FIELDS } from "../../../strings";
|
|
2
|
+
import { OverrideEntry } from "../entry";
|
|
3
|
+
import type { InputValidator } from "./base";
|
|
4
|
+
import { OverrideField } from "./base";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The entry's pattern key. Renames are handled by the editor
|
|
8
|
+
* (`OverrideEntryListEditor.applyFieldChange`), so {@link apply}
|
|
9
|
+
* inherits the base no-op.
|
|
10
|
+
*/
|
|
11
|
+
export class PatternField extends OverrideField {
|
|
12
|
+
readonly id = "pattern";
|
|
13
|
+
readonly type = "input";
|
|
14
|
+
readonly field = FIELDS.pattern;
|
|
15
|
+
|
|
16
|
+
displayValue(entry: OverrideEntry): string {
|
|
17
|
+
return entry.pattern;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
readonly validate: InputValidator = (raw) => {
|
|
21
|
+
const trimmed = raw.trim();
|
|
22
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ModelOverride } from "../../../../interfaces/settings";
|
|
2
|
+
import { FIELDS } from "../../../strings";
|
|
3
|
+
import { OverrideEntry } from "../entry";
|
|
4
|
+
import type { InputValidator } from "./base";
|
|
5
|
+
import { OverrideField } from "./base";
|
|
6
|
+
|
|
7
|
+
/** The override's `reasoning` flag (finite: true/false, default true). */
|
|
8
|
+
export class ReasoningField extends OverrideField {
|
|
9
|
+
readonly id = "reasoning";
|
|
10
|
+
readonly type = "finite";
|
|
11
|
+
readonly field = FIELDS.reasoning;
|
|
12
|
+
override readonly options = ["true", "false"];
|
|
13
|
+
|
|
14
|
+
displayValue(entry: OverrideEntry): string {
|
|
15
|
+
return entry.override.reasoning === false ? "false" : "true";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
readonly validate: InputValidator = (raw) =>
|
|
19
|
+
this.options?.includes(raw) ? raw : null;
|
|
20
|
+
|
|
21
|
+
apply(override: ModelOverride, value: string): void {
|
|
22
|
+
if (value === "true") override.reasoning = true;
|
|
23
|
+
else if (value === "false") override.reasoning = false;
|
|
24
|
+
else delete override.reasoning;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Appears in the summary only when explicitly set. */
|
|
28
|
+
override summaryPart(override: ModelOverride): string | null {
|
|
29
|
+
return override.reasoning === undefined
|
|
30
|
+
? null
|
|
31
|
+
: `reasoning: ${override.reasoning}`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -2,7 +2,8 @@ import type { SettingItem } from "@earendil-works/pi-tui";
|
|
|
2
2
|
import { FieldMessages, TITLES } from "../../strings";
|
|
3
3
|
import { ItemBuilder } from "../itemBuilder";
|
|
4
4
|
import { OverrideEntry } from "./entry";
|
|
5
|
-
import {
|
|
5
|
+
import type { OverrideField } from "./fields/base";
|
|
6
|
+
import { OverrideFields } from "./fields/index";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Builds `SettingItem` objects for override entry field submenus.
|
|
@@ -20,11 +21,7 @@ export class OverrideItemBuilder extends ItemBuilder<
|
|
|
20
21
|
* an `InputDialog` submenu (validating against the field definition)
|
|
21
22
|
* for "input" fields.
|
|
22
23
|
*/
|
|
23
|
-
protected decorate(
|
|
24
|
-
def: OverrideField,
|
|
25
|
-
entry: OverrideEntry,
|
|
26
|
-
base: SettingItem,
|
|
27
|
-
): SettingItem {
|
|
24
|
+
protected decorate(def: OverrideField, base: SettingItem): SettingItem {
|
|
28
25
|
if (def.type === "finite") {
|
|
29
26
|
return { ...base, values: [...(def.options ?? [])] };
|
|
30
27
|
}
|
|
@@ -18,11 +18,7 @@ export class ServerItemBuilder extends ItemBuilder<LlamaServer, ServerField> {
|
|
|
18
18
|
* Adds the field-edit submenu: an `InputDialog` validating against the
|
|
19
19
|
* field definition.
|
|
20
20
|
*/
|
|
21
|
-
protected decorate(
|
|
22
|
-
def: ServerField,
|
|
23
|
-
server: LlamaServer,
|
|
24
|
-
base: SettingItem,
|
|
25
|
-
): SettingItem {
|
|
21
|
+
protected decorate(def: ServerField, base: SettingItem): SettingItem {
|
|
26
22
|
return {
|
|
27
23
|
...base,
|
|
28
24
|
submenu: this.dialogs.inputSubmenu(
|
|
@@ -3,11 +3,12 @@ import type { SettingItem, SettingsList } from "@earendil-works/pi-tui";
|
|
|
3
3
|
import { SERVER_TIMEOUT } from "../../../constants";
|
|
4
4
|
import type { LlamaServer } from "../../../interfaces/settings";
|
|
5
5
|
import type { LlamaSettingsManager } from "../../../managers/settings";
|
|
6
|
-
import {
|
|
6
|
+
import { ServerIds } from "../../../utils/serverIds";
|
|
7
|
+
import { TITLES, authRequiredMessage } from "../../strings";
|
|
7
8
|
import type { ServerSettingsListOptions } from "../editorOptions";
|
|
8
9
|
import { ListEditor } from "../listEditor";
|
|
9
10
|
import { SettingsListFactory } from "../settingsListFactory";
|
|
10
|
-
import {
|
|
11
|
+
import { ServerFields } from "./fields";
|
|
11
12
|
import { ServerItemBuilder } from "./itemBuilder";
|
|
12
13
|
import { ServerDisplay } from "./utils";
|
|
13
14
|
import { ServerWizard } from "./wizard";
|
|
@@ -58,6 +59,15 @@ export class ServerSettingsList extends ListEditor<ServerSettingsListOptions> {
|
|
|
58
59
|
done: () => done(undefined),
|
|
59
60
|
onError: (message) => ui.notify(message, "error"),
|
|
60
61
|
serverTimeout,
|
|
62
|
+
authResolver: async (server) => {
|
|
63
|
+
// Mirror Server.getApiKey(): use ServerIds.resolve to get
|
|
64
|
+
// the provider ID (customId or llama-server=<url>), then
|
|
65
|
+
// resolve the key from the credential store.
|
|
66
|
+
return settings.resolveApiKey(
|
|
67
|
+
ServerIds.resolve(server.url, server.id),
|
|
68
|
+
);
|
|
69
|
+
},
|
|
70
|
+
ui,
|
|
61
71
|
}),
|
|
62
72
|
);
|
|
63
73
|
}
|
|
@@ -67,17 +77,24 @@ export class ServerSettingsList extends ListEditor<ServerSettingsListOptions> {
|
|
|
67
77
|
protected async buildSettingsList(): Promise<SettingsList> {
|
|
68
78
|
const builder = new ServerItemBuilder(this.dialogs);
|
|
69
79
|
const serverTimeout = this.options.serverTimeout ?? SERVER_TIMEOUT;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
)
|
|
80
|
+
// Run auth + health probes in parallel; ⛔ wins if auth fails,
|
|
81
|
+
// otherwise fall back to the health emoji.
|
|
82
|
+
const probes = await Promise.all(
|
|
83
|
+
this.options.servers.map(async (server) => {
|
|
84
|
+
const apiKey = (await this.options.authResolver?.(server)) ?? "";
|
|
85
|
+
const [authEmoji, healthEmoji] = await Promise.all([
|
|
86
|
+
ServerDisplay.authEmoji(server.url, apiKey, serverTimeout),
|
|
87
|
+
ServerDisplay.healthEmoji(server.url, serverTimeout),
|
|
88
|
+
]);
|
|
89
|
+
return authEmoji || healthEmoji;
|
|
90
|
+
}),
|
|
74
91
|
);
|
|
75
92
|
const items: SettingItem[] = this.options.servers.map((server, i) =>
|
|
76
93
|
builder.buildRow(
|
|
77
94
|
server,
|
|
78
95
|
i,
|
|
79
96
|
(field, value) => this.handleFieldChange(field, value),
|
|
80
|
-
|
|
97
|
+
probes[i],
|
|
81
98
|
// Opening delegates input to the submenu; closing flushes a
|
|
82
99
|
// rebuild deferred by a field commit (see commitFieldChange)
|
|
83
100
|
(open) => this.trackSubmenu(open),
|
|
@@ -92,10 +109,13 @@ export class ServerSettingsList extends ListEditor<ServerSettingsListOptions> {
|
|
|
92
109
|
/** Handle a field commit from a server row's submenu. */
|
|
93
110
|
private handleFieldChange(field: string, value: string): void {
|
|
94
111
|
const idx = this.selectedIndex;
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
112
|
+
const server = this.options.servers[idx];
|
|
113
|
+
const updated = server
|
|
114
|
+
? ServerFields.byId(field).apply(server, value)
|
|
115
|
+
: server;
|
|
116
|
+
const next = server
|
|
117
|
+
? this.options.servers.map((s, i) => (i === idx ? updated : s))
|
|
118
|
+
: this.options.servers;
|
|
99
119
|
|
|
100
120
|
this.commitFieldChange(next, idx, () => {
|
|
101
121
|
// Refresh the row's suffix in place; the full rebuild (row labels
|
|
@@ -134,10 +154,6 @@ export class ServerSettingsList extends ListEditor<ServerSettingsListOptions> {
|
|
|
134
154
|
|
|
135
155
|
protected readonly emptyHintKey = "emptyServers" as const;
|
|
136
156
|
|
|
137
|
-
protected getCurrentCount(): number {
|
|
138
|
-
return this.options.servers.length;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
157
|
protected getRowId(index: number): string {
|
|
142
158
|
return `server-${index}`;
|
|
143
159
|
}
|
|
@@ -157,5 +173,25 @@ export class ServerSettingsList extends ListEditor<ServerSettingsListOptions> {
|
|
|
157
173
|
await this.persistSnapshot(next, () => {
|
|
158
174
|
void this.rebuildList(next.length - 1);
|
|
159
175
|
});
|
|
176
|
+
|
|
177
|
+
// Warn the user if the newly added server requires an API key
|
|
178
|
+
await this.warnIfAuthRequired(server);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Probes the server to check if it requires an API key. If so, notifies
|
|
183
|
+
* the user so they know to configure one via `/login` or `auth.json`.
|
|
184
|
+
*/
|
|
185
|
+
private async warnIfAuthRequired(server: LlamaServer): Promise<void> {
|
|
186
|
+
const ui = this.options.ui;
|
|
187
|
+
if (!ui) return;
|
|
188
|
+
|
|
189
|
+
const serverTimeout = this.options.serverTimeout ?? SERVER_TIMEOUT;
|
|
190
|
+
const apiKey = (await this.options.authResolver?.(server)) ?? "";
|
|
191
|
+
|
|
192
|
+
if (await ServerDisplay.requiresApiKey(server.url, apiKey, serverTimeout)) {
|
|
193
|
+
const providerId = ServerIds.resolve(server.url, server.id);
|
|
194
|
+
ui.notify(authRequiredMessage(server.url, providerId), "warning");
|
|
195
|
+
}
|
|
160
196
|
}
|
|
161
197
|
}
|