@aliou/pi-ts-aperture 0.3.1 → 0.4.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/package.json +9 -8
- package/src/commands/settings.ts +36 -0
- package/src/commands/setup.ts +144 -237
- package/src/config.ts +3 -0
- package/src/core/index.ts +7 -0
- package/src/core/plan.test.ts +253 -0
- package/src/core/plan.ts +107 -0
- package/src/core/types.ts +33 -0
- package/src/core/url.test.ts +130 -0
- package/src/core/url.ts +42 -0
- package/src/index.ts +88 -14
- package/src/lib/health.ts +15 -0
- package/src/providers/aperture.ts +40 -128
- package/src/lib/aperture-api.ts +0 -32
- package/src/providers/model-config.ts +0 -54
- package/src/state/provider-model-cache.ts +0 -18
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aliou/pi-ts-aperture",
|
|
3
3
|
"description": "Route Pi LLM providers through Tailscale Aperture",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"private": false,
|
|
@@ -29,12 +29,12 @@
|
|
|
29
29
|
"video": "https://assets.aliou.me/pi-extensions/demos/pi-ts-aperture.mp4"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@aliou/pi-utils-settings": "^0.
|
|
32
|
+
"@aliou/pi-utils-settings": "^0.12.0"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@mariozechner/pi-ai": "
|
|
36
|
-
"@mariozechner/pi-coding-agent": "
|
|
37
|
-
"@mariozechner/pi-tui": "
|
|
35
|
+
"@mariozechner/pi-ai": "0.64.0",
|
|
36
|
+
"@mariozechner/pi-coding-agent": "0.64.0",
|
|
37
|
+
"@mariozechner/pi-tui": "0.64.0"
|
|
38
38
|
},
|
|
39
39
|
"peerDependenciesMeta": {
|
|
40
40
|
"@mariozechner/pi-coding-agent": {
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"@aliou/biome-plugins": "^0.3.2",
|
|
52
52
|
"@biomejs/biome": "^2.3.13",
|
|
53
53
|
"@changesets/cli": "^2.27.11",
|
|
54
|
-
"@mariozechner/pi-coding-agent": "0.
|
|
54
|
+
"@mariozechner/pi-coding-agent": "0.64.0",
|
|
55
55
|
"@sinclair/typebox": "^0.34.48",
|
|
56
56
|
"@types/node": "^25.0.10",
|
|
57
57
|
"@vitest/coverage-v8": "^4.0.18",
|
|
@@ -67,7 +67,8 @@
|
|
|
67
67
|
"changeset": "changeset",
|
|
68
68
|
"version": "changeset version",
|
|
69
69
|
"release": "pnpm changeset publish",
|
|
70
|
-
"test": "vitest run
|
|
71
|
-
"test:watch": "vitest
|
|
70
|
+
"test": "vitest run",
|
|
71
|
+
"test:watch": "vitest",
|
|
72
|
+
"test:e2e": "vitest run tests/e2e.test.ts"
|
|
72
73
|
}
|
|
73
74
|
}
|
package/src/commands/settings.ts
CHANGED
|
@@ -37,6 +37,9 @@ export function registerApertureSettings(
|
|
|
37
37
|
|
|
38
38
|
const providers = tabConfig?.providers ?? resolved.providers;
|
|
39
39
|
|
|
40
|
+
const checkGatewayModels: string[] =
|
|
41
|
+
tabConfig?.checkGatewayModels ?? resolved.checkGatewayModels;
|
|
42
|
+
|
|
40
43
|
return [
|
|
41
44
|
{
|
|
42
45
|
label: "Connection",
|
|
@@ -51,6 +54,39 @@ export function registerApertureSettings(
|
|
|
51
54
|
values: undefined,
|
|
52
55
|
submenu: undefined,
|
|
53
56
|
},
|
|
57
|
+
{
|
|
58
|
+
id: "checkGatewayModels",
|
|
59
|
+
label: "Gateway model checking",
|
|
60
|
+
description:
|
|
61
|
+
"Providers for which gateway model availability is checked",
|
|
62
|
+
currentValue:
|
|
63
|
+
checkGatewayModels.length > 0
|
|
64
|
+
? `${checkGatewayModels.length} provider(s)`
|
|
65
|
+
: "disabled",
|
|
66
|
+
values: undefined,
|
|
67
|
+
submenu: (_val, submenuDone) => {
|
|
68
|
+
let latest = [...checkGatewayModels];
|
|
69
|
+
return new ArrayEditor({
|
|
70
|
+
label: "Gateway-checked providers",
|
|
71
|
+
items: [...checkGatewayModels],
|
|
72
|
+
theme: settingsTheme,
|
|
73
|
+
onSave: (items) => {
|
|
74
|
+
latest = items;
|
|
75
|
+
const updated = structuredClone(
|
|
76
|
+
tabConfig ?? {},
|
|
77
|
+
) as ApertureConfig;
|
|
78
|
+
setNestedValue(updated, "checkGatewayModels", items);
|
|
79
|
+
setDraft(updated);
|
|
80
|
+
},
|
|
81
|
+
onDone: () =>
|
|
82
|
+
submenuDone(
|
|
83
|
+
latest.length > 0
|
|
84
|
+
? `${latest.length} provider(s)`
|
|
85
|
+
: "disabled",
|
|
86
|
+
),
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
},
|
|
54
90
|
],
|
|
55
91
|
},
|
|
56
92
|
{
|
package/src/commands/setup.ts
CHANGED
|
@@ -2,253 +2,135 @@
|
|
|
2
2
|
* aperture:setup -- interactive wizard for configuring Aperture.
|
|
3
3
|
*
|
|
4
4
|
* Steps:
|
|
5
|
-
* 1.
|
|
6
|
-
* 2.
|
|
7
|
-
* 3. Save config and register providers
|
|
5
|
+
* 1. URL input (health check runs inline on Enter, auto-advances on success)
|
|
6
|
+
* 2. Provider selection with per-provider "verify models" sub-option
|
|
8
7
|
*/
|
|
9
8
|
|
|
10
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
FuzzyMultiSelector,
|
|
11
|
+
type FuzzyMultiSelectorItem,
|
|
12
|
+
getSettingsTheme,
|
|
13
|
+
type SettingsTheme,
|
|
14
|
+
Wizard,
|
|
15
|
+
type WizardStepContext,
|
|
16
|
+
} from "@aliou/pi-utils-settings";
|
|
11
17
|
import type {
|
|
12
18
|
ExtensionAPI,
|
|
13
19
|
ExtensionContext,
|
|
14
20
|
} from "@mariozechner/pi-coding-agent";
|
|
15
|
-
import { getSettingsListTheme } from "@mariozechner/pi-coding-agent";
|
|
16
21
|
import type { Component, TUI } from "@mariozechner/pi-tui";
|
|
17
|
-
import { Input
|
|
22
|
+
import { Input } from "@mariozechner/pi-tui";
|
|
18
23
|
import { configLoader } from "../config";
|
|
24
|
+
import { normalizeInputUrl } from "../core";
|
|
19
25
|
import { checkApertureHealth } from "../lib/health";
|
|
20
26
|
|
|
21
|
-
function normalizeUrl(url: string): string {
|
|
22
|
-
let result = url.trim();
|
|
23
|
-
if (!result) return result;
|
|
24
|
-
if (!result.startsWith("http://") && !result.startsWith("https://")) {
|
|
25
|
-
result = `http://${result}`;
|
|
26
|
-
}
|
|
27
|
-
return result.replace(/\/v1\/?$/, "").replace(/\/+$/, "");
|
|
28
|
-
}
|
|
29
|
-
|
|
30
27
|
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
31
28
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
private theme:
|
|
39
|
-
private url: string;
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Step 1: URL input with inline health check
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
class UrlStep implements Component {
|
|
34
|
+
private input: Input;
|
|
35
|
+
private theme: SettingsTheme;
|
|
40
36
|
private tui: TUI;
|
|
41
|
-
|
|
42
|
-
private
|
|
37
|
+
private wizCtx: WizardStepContext;
|
|
38
|
+
private onUrl: (url: string) => void;
|
|
39
|
+
private readonly placeholder = "ai.pango-lin.ts.net";
|
|
40
|
+
|
|
41
|
+
private state: "idle" | "checking" | "ok" | "error" = "idle";
|
|
42
|
+
private errorMessage = "";
|
|
43
43
|
private frame = 0;
|
|
44
|
-
private timer: ReturnType<typeof setInterval
|
|
45
|
-
private result: { ok: boolean; error?: string } | null = null;
|
|
44
|
+
private timer: ReturnType<typeof setInterval> | null = null;
|
|
46
45
|
|
|
47
46
|
constructor(
|
|
48
|
-
theme:
|
|
49
|
-
url: string,
|
|
47
|
+
theme: SettingsTheme,
|
|
50
48
|
tui: TUI,
|
|
51
|
-
|
|
49
|
+
currentValue: string,
|
|
50
|
+
wizCtx: WizardStepContext,
|
|
51
|
+
onUrl: (url: string) => void,
|
|
52
52
|
) {
|
|
53
53
|
this.theme = theme;
|
|
54
|
-
this.url = url;
|
|
55
54
|
this.tui = tui;
|
|
56
|
-
this.
|
|
55
|
+
this.wizCtx = wizCtx;
|
|
56
|
+
this.onUrl = onUrl;
|
|
57
|
+
this.input = new Input();
|
|
58
|
+
if (currentValue) {
|
|
59
|
+
this.input.setValue(currentValue);
|
|
60
|
+
}
|
|
61
|
+
this.input.onSubmit = () => this.submit();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private submit(): void {
|
|
65
|
+
const value = this.input.getValue().trim();
|
|
66
|
+
if (!value || this.state === "checking") return;
|
|
67
|
+
|
|
68
|
+
const url = normalizeInputUrl(value);
|
|
69
|
+
this.state = "checking";
|
|
70
|
+
this.frame = 0;
|
|
57
71
|
|
|
58
|
-
// Animate spinner at ~80ms per frame.
|
|
59
72
|
this.timer = setInterval(() => {
|
|
60
73
|
this.frame = (this.frame + 1) % SPINNER_FRAMES.length;
|
|
61
74
|
this.tui.requestRender();
|
|
62
75
|
}, 80);
|
|
63
76
|
|
|
64
|
-
// Fire the check.
|
|
65
77
|
checkApertureHealth(url).then((res) => {
|
|
66
|
-
clearInterval(this.timer);
|
|
67
|
-
this.
|
|
68
|
-
this.tui.requestRender();
|
|
78
|
+
if (this.timer) clearInterval(this.timer);
|
|
79
|
+
this.timer = null;
|
|
69
80
|
|
|
70
|
-
// On success, auto-advance after a brief pause.
|
|
71
|
-
// On failure, wait for user input (see handleInput).
|
|
72
81
|
if (res.ok) {
|
|
73
|
-
|
|
82
|
+
this.state = "ok";
|
|
83
|
+
this.onUrl(url);
|
|
84
|
+
this.wizCtx.markComplete();
|
|
85
|
+
this.tui.requestRender();
|
|
86
|
+
setTimeout(() => this.wizCtx.goNext(), 400);
|
|
87
|
+
} else {
|
|
88
|
+
this.state = "error";
|
|
89
|
+
this.errorMessage = res.error ?? "unknown error";
|
|
90
|
+
this.tui.requestRender();
|
|
74
91
|
}
|
|
75
92
|
});
|
|
76
93
|
}
|
|
77
94
|
|
|
78
|
-
render(_width: number): string[] {
|
|
79
|
-
const lines: string[] = [];
|
|
80
|
-
lines.push(this.theme.label(" Aperture Setup", true));
|
|
81
|
-
lines.push("");
|
|
82
|
-
|
|
83
|
-
if (!this.result) {
|
|
84
|
-
const spinner = SPINNER_FRAMES[this.frame];
|
|
85
|
-
lines.push(
|
|
86
|
-
this.theme.hint(` ${spinner} Checking connection to ${this.url}...`),
|
|
87
|
-
);
|
|
88
|
-
} else if (this.result.ok) {
|
|
89
|
-
lines.push(this.theme.hint(` Connected to ${this.url}`));
|
|
90
|
-
} else {
|
|
91
|
-
lines.push(
|
|
92
|
-
this.theme.hint(` Could not reach ${this.url}: ${this.result.error}`),
|
|
93
|
-
);
|
|
94
|
-
lines.push("");
|
|
95
|
-
lines.push(
|
|
96
|
-
this.theme.hint(
|
|
97
|
-
" Make sure the URL is correct and you are connected to the tailnet.",
|
|
98
|
-
),
|
|
99
|
-
);
|
|
100
|
-
lines.push("");
|
|
101
|
-
lines.push(this.theme.hint(" Enter: try another URL · Esc: cancel"));
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
lines.push("");
|
|
105
|
-
return lines;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
invalidate() {}
|
|
109
|
-
|
|
110
|
-
handleInput(data: string) {
|
|
111
|
-
// Only handle input after a failed check.
|
|
112
|
-
if (!this.result || this.result.ok) return;
|
|
113
|
-
|
|
114
|
-
if (matchesKey(data, Key.enter)) {
|
|
115
|
-
this.done(false); // retry
|
|
116
|
-
} else if (matchesKey(data, Key.escape)) {
|
|
117
|
-
this.done(undefined); // cancel wizard
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
dispose() {
|
|
122
|
-
clearInterval(this.timer);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Simple input prompt component for the base URL step.
|
|
128
|
-
*/
|
|
129
|
-
class UrlPrompt implements Component {
|
|
130
|
-
private input: Input;
|
|
131
|
-
private done: (value: string | undefined) => void;
|
|
132
|
-
private theme: ReturnType<typeof getSettingsListTheme>;
|
|
133
|
-
private placeholder = "ai.pango-lin.ts.net";
|
|
134
|
-
|
|
135
|
-
constructor(
|
|
136
|
-
theme: ReturnType<typeof getSettingsListTheme>,
|
|
137
|
-
currentValue: string,
|
|
138
|
-
done: (value: string | undefined) => void,
|
|
139
|
-
) {
|
|
140
|
-
this.theme = theme;
|
|
141
|
-
this.done = done;
|
|
142
|
-
this.input = new Input();
|
|
143
|
-
if (currentValue) {
|
|
144
|
-
this.input.setValue(currentValue);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
this.input.onSubmit = () => {
|
|
148
|
-
const value = this.input.getValue().trim();
|
|
149
|
-
if (!value) return;
|
|
150
|
-
this.done(normalizeUrl(value));
|
|
151
|
-
};
|
|
152
|
-
this.input.onEscape = () => {
|
|
153
|
-
this.done(undefined);
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
|
|
157
95
|
render(width: number): string[] {
|
|
158
96
|
const lines: string[] = [];
|
|
159
|
-
|
|
160
|
-
lines.push("");
|
|
97
|
+
|
|
161
98
|
lines.push(
|
|
162
99
|
this.theme.hint(` Aperture base URL (e.g. ${this.placeholder}):`),
|
|
163
100
|
);
|
|
164
101
|
lines.push(` ${this.input.render(width - 4).join("")}`);
|
|
165
102
|
lines.push("");
|
|
166
|
-
lines.push(this.theme.hint(" Enter: confirm · Esc: cancel"));
|
|
167
|
-
return lines;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
invalidate() {}
|
|
171
|
-
|
|
172
|
-
handleInput(data: string) {
|
|
173
|
-
this.input.handleInput(data);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Provider multi-select component. Uses FuzzySelector for search,
|
|
179
|
-
* tracks selected providers, and lets the user confirm with Ctrl+S.
|
|
180
|
-
*/
|
|
181
|
-
class ProviderMultiSelect implements Component {
|
|
182
|
-
private allProviders: string[];
|
|
183
|
-
private selected: Set<string>;
|
|
184
|
-
private theme: ReturnType<typeof getSettingsListTheme>;
|
|
185
|
-
private done: (value: string[] | undefined) => void;
|
|
186
|
-
private fuzzy: FuzzySelector;
|
|
187
103
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
this.theme = theme;
|
|
197
|
-
this.done = done;
|
|
198
|
-
|
|
199
|
-
this.fuzzy = new FuzzySelector({
|
|
200
|
-
label: "Select providers (Enter to toggle, Ctrl+S to confirm)",
|
|
201
|
-
items: this.allProviders,
|
|
202
|
-
theme,
|
|
203
|
-
onSelect: (value) => {
|
|
204
|
-
if (this.selected.has(value)) {
|
|
205
|
-
this.selected.delete(value);
|
|
206
|
-
} else {
|
|
207
|
-
this.selected.add(value);
|
|
208
|
-
}
|
|
209
|
-
},
|
|
210
|
-
onDone: () => {
|
|
211
|
-
this.done(undefined);
|
|
212
|
-
},
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
render(width: number): string[] {
|
|
217
|
-
const lines = this.fuzzy.render(width);
|
|
218
|
-
|
|
219
|
-
// Append selected providers summary
|
|
220
|
-
if (this.selected.size > 0) {
|
|
221
|
-
lines.push("");
|
|
222
|
-
lines.push(this.theme.hint(` Selected (${this.selected.size}):`));
|
|
223
|
-
for (const p of this.selected) {
|
|
224
|
-
lines.push(` ${this.theme.value(p, false)}`);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Replace the hint line from FuzzySelector
|
|
229
|
-
const hintIndex = lines.findIndex((l) => l.includes("Type to search"));
|
|
230
|
-
if (hintIndex !== -1) {
|
|
231
|
-
lines[hintIndex] = this.theme.hint(
|
|
232
|
-
" Type to search · Enter: toggle · Ctrl+S: confirm · Esc: cancel",
|
|
233
|
-
);
|
|
104
|
+
if (this.state === "checking") {
|
|
105
|
+
const spinner = SPINNER_FRAMES[this.frame];
|
|
106
|
+
lines.push(this.theme.hint(` ${spinner} Checking connection...`));
|
|
107
|
+
} else if (this.state === "ok") {
|
|
108
|
+
lines.push(this.theme.hint(" Connected."));
|
|
109
|
+
} else if (this.state === "error") {
|
|
110
|
+
lines.push(this.theme.hint(` Could not connect: ${this.errorMessage}`));
|
|
111
|
+
lines.push(this.theme.hint(" Fix the URL and press Enter to retry."));
|
|
234
112
|
}
|
|
235
113
|
|
|
236
114
|
return lines;
|
|
237
115
|
}
|
|
238
116
|
|
|
239
|
-
invalidate() {
|
|
240
|
-
|
|
117
|
+
invalidate(): void {}
|
|
118
|
+
|
|
119
|
+
handleInput(data: string): void {
|
|
120
|
+
if (this.state === "checking") return;
|
|
121
|
+
this.state = "idle";
|
|
122
|
+
this.input.handleInput(data);
|
|
241
123
|
}
|
|
242
124
|
|
|
243
|
-
|
|
244
|
-
if (
|
|
245
|
-
this.done([...this.selected]);
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
this.fuzzy.handleInput(data);
|
|
125
|
+
dispose(): void {
|
|
126
|
+
if (this.timer) clearInterval(this.timer);
|
|
249
127
|
}
|
|
250
128
|
}
|
|
251
129
|
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// Command registration
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
|
|
252
134
|
export function registerSetupCommand(
|
|
253
135
|
pi: ExtensionAPI,
|
|
254
136
|
onConfigChange: (ctx: ExtensionContext) => void,
|
|
@@ -265,56 +147,81 @@ export function registerSetupCommand(
|
|
|
265
147
|
}
|
|
266
148
|
|
|
267
149
|
const config = configLoader.getConfig();
|
|
268
|
-
const
|
|
150
|
+
const checkGatewayProviders = config.checkGatewayModels ?? [];
|
|
269
151
|
|
|
270
|
-
// Step 1: base URL + health check loop.
|
|
271
|
-
// On failure, loop back to the URL prompt so the user can retry.
|
|
272
|
-
let baseUrl: string | undefined;
|
|
273
|
-
while (true) {
|
|
274
|
-
baseUrl = await ctx.ui.custom<string | undefined>(
|
|
275
|
-
(_tui, _theme, _kb, done) => {
|
|
276
|
-
return new UrlPrompt(
|
|
277
|
-
settingsTheme,
|
|
278
|
-
baseUrl ?? config.baseUrl,
|
|
279
|
-
done,
|
|
280
|
-
);
|
|
281
|
-
},
|
|
282
|
-
);
|
|
283
|
-
|
|
284
|
-
if (!baseUrl) return;
|
|
285
|
-
const urlToCheck = baseUrl;
|
|
286
|
-
|
|
287
|
-
const result = await ctx.ui.custom<boolean | undefined>(
|
|
288
|
-
(tui, _theme, _kb, done) => {
|
|
289
|
-
return new HealthCheckSpinner(settingsTheme, urlToCheck, tui, done);
|
|
290
|
-
},
|
|
291
|
-
);
|
|
292
|
-
|
|
293
|
-
if (result === true) break; // healthy, proceed
|
|
294
|
-
if (result === undefined) return; // cancelled
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// Step 2: select providers
|
|
298
|
-
// Use model registry so custom/extension providers are included.
|
|
299
152
|
const knownProviders = Array.from(
|
|
300
153
|
new Set(ctx.modelRegistry.getAll().map((model) => model.provider)),
|
|
301
154
|
).sort((a, b) => a.localeCompare(b));
|
|
302
155
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
156
|
+
let baseUrl = config.baseUrl;
|
|
157
|
+
|
|
158
|
+
const providerItems: FuzzyMultiSelectorItem[] = knownProviders.map(
|
|
159
|
+
(p) => ({
|
|
160
|
+
label: p,
|
|
161
|
+
checked: config.providers.includes(p),
|
|
162
|
+
subOptions: [
|
|
163
|
+
{
|
|
164
|
+
label: "verify models on gateway",
|
|
165
|
+
description:
|
|
166
|
+
"Warn at startup if this provider's models are missing from the Aperture gateway",
|
|
167
|
+
checked: checkGatewayProviders.includes(p),
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
}),
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
const confirmed = await ctx.ui.custom<boolean | undefined>(
|
|
174
|
+
(tui, theme, _kb, done) => {
|
|
175
|
+
const settingsTheme = getSettingsTheme(theme);
|
|
176
|
+
|
|
177
|
+
return new Wizard({
|
|
178
|
+
title: "Aperture Setup",
|
|
179
|
+
theme: settingsTheme,
|
|
180
|
+
minContentHeight: 16,
|
|
181
|
+
steps: [
|
|
182
|
+
{
|
|
183
|
+
label: "URL",
|
|
184
|
+
build: (wCtx: WizardStepContext) =>
|
|
185
|
+
new UrlStep(settingsTheme, tui, baseUrl, wCtx, (url) => {
|
|
186
|
+
baseUrl = url;
|
|
187
|
+
}),
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
label: "Providers",
|
|
191
|
+
build: (wCtx: WizardStepContext) => {
|
|
192
|
+
wCtx.markComplete();
|
|
193
|
+
return new FuzzyMultiSelector({
|
|
194
|
+
label: "Providers to route through Aperture",
|
|
195
|
+
items: providerItems,
|
|
196
|
+
theme: settingsTheme,
|
|
197
|
+
showHints: false,
|
|
198
|
+
showCount: false,
|
|
199
|
+
maxVisible: 7,
|
|
200
|
+
});
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
onComplete: () => done(true),
|
|
205
|
+
onCancel: () => done(undefined),
|
|
206
|
+
});
|
|
311
207
|
},
|
|
312
208
|
);
|
|
313
209
|
|
|
314
|
-
if (!
|
|
210
|
+
if (!confirmed) return;
|
|
211
|
+
|
|
212
|
+
const providers = providerItems
|
|
213
|
+
.filter((i) => i.checked)
|
|
214
|
+
.map((i) => i.label);
|
|
215
|
+
|
|
216
|
+
const checkGatewayModels = providerItems
|
|
217
|
+
.filter((i) => i.checked && i.subOptions?.[0]?.checked)
|
|
218
|
+
.map((i) => i.label);
|
|
315
219
|
|
|
316
|
-
|
|
317
|
-
|
|
220
|
+
await configLoader.save("global", {
|
|
221
|
+
baseUrl,
|
|
222
|
+
providers,
|
|
223
|
+
checkGatewayModels,
|
|
224
|
+
});
|
|
318
225
|
onConfigChange(ctx);
|
|
319
226
|
ctx.ui.notify(
|
|
320
227
|
`Aperture configured: ${providers.length} provider(s) via ${baseUrl}`,
|
package/src/config.ts
CHANGED
|
@@ -10,16 +10,19 @@ import { ConfigLoader } from "@aliou/pi-utils-settings";
|
|
|
10
10
|
export interface ApertureConfig {
|
|
11
11
|
baseUrl?: string;
|
|
12
12
|
providers?: string[];
|
|
13
|
+
checkGatewayModels?: string[];
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
export interface ResolvedConfig {
|
|
16
17
|
baseUrl: string;
|
|
17
18
|
providers: string[];
|
|
19
|
+
checkGatewayModels: string[];
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
const DEFAULT_CONFIG: ResolvedConfig = {
|
|
21
23
|
baseUrl: "",
|
|
22
24
|
providers: [],
|
|
25
|
+
checkGatewayModels: [],
|
|
23
26
|
};
|
|
24
27
|
|
|
25
28
|
export const configLoader = new ConfigLoader<ApertureConfig, ResolvedConfig>(
|