@aliou/pi-ts-aperture 0.5.1 → 0.6.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 +119 -21
- package/extensions/aperture/dedicated/api-routing.ts +66 -0
- package/extensions/aperture/dedicated/model-defaults.ts +48 -0
- package/extensions/aperture/dedicated/runtime.ts +87 -0
- package/extensions/aperture/index.ts +78 -0
- package/extensions/aperture/onboarding/index.ts +25 -0
- package/extensions/aperture/onboarding/onboarding.ts +892 -0
- package/extensions/aperture/onboarding/setup-command.ts +53 -0
- package/extensions/aperture/onboarding/setup-wizard.ts +134 -0
- package/extensions/aperture/proxy/runtime.ts +160 -0
- package/extensions/aperture/settings-command.ts +369 -0
- package/extensions/aperture/shared/config/defaults.ts +17 -0
- package/extensions/aperture/shared/config/loader.ts +21 -0
- package/extensions/aperture/shared/config/migration/001-legacy-to-v0-6.ts +45 -0
- package/extensions/aperture/shared/config/migration/002-mode-to-capabilities.ts +20 -0
- package/extensions/aperture/shared/config/migration/003-normalize-capabilities.ts +26 -0
- package/extensions/aperture/shared/config/migration/index.ts +15 -0
- package/extensions/aperture/shared/config/types.ts +57 -0
- package/extensions/aperture/shared/sync-bus.ts +12 -0
- package/{src/lib → extensions/aperture/shared}/types.ts +1 -1
- package/package.json +37 -27
- package/src/api/client.ts +139 -0
- package/src/api/types.ts +26 -0
- package/src/provider-mapping.ts +91 -0
- package/src/url.ts +52 -0
- package/src/commands/settings.ts +0 -135
- package/src/commands/setup.ts +0 -232
- package/src/extension/runtime.test.ts +0 -121
- package/src/extension/runtime.ts +0 -144
- package/src/index.ts +0 -97
- package/src/lib/config.ts +0 -32
- package/src/lib/gateway.ts +0 -61
- package/src/lib/url.ts +0 -42
package/src/commands/setup.ts
DELETED
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* aperture:setup -- interactive wizard for configuring Aperture.
|
|
3
|
-
*
|
|
4
|
-
* Steps:
|
|
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
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
FuzzyMultiSelector,
|
|
11
|
-
type FuzzyMultiSelectorItem,
|
|
12
|
-
getSettingsTheme,
|
|
13
|
-
type SettingsTheme,
|
|
14
|
-
Wizard,
|
|
15
|
-
type WizardStepContext,
|
|
16
|
-
} from "@aliou/pi-utils-settings";
|
|
17
|
-
import type {
|
|
18
|
-
ExtensionAPI,
|
|
19
|
-
ExtensionContext,
|
|
20
|
-
} from "@mariozechner/pi-coding-agent";
|
|
21
|
-
import type { Component, TUI } from "@mariozechner/pi-tui";
|
|
22
|
-
import { Input } from "@mariozechner/pi-tui";
|
|
23
|
-
import { configLoader } from "../lib/config";
|
|
24
|
-
import { checkApertureHealth } from "../lib/gateway";
|
|
25
|
-
import { normalizeInputUrl } from "../lib/url";
|
|
26
|
-
|
|
27
|
-
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
28
|
-
|
|
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;
|
|
36
|
-
private tui: TUI;
|
|
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
|
-
private frame = 0;
|
|
44
|
-
private timer: ReturnType<typeof setInterval> | null = null;
|
|
45
|
-
|
|
46
|
-
constructor(
|
|
47
|
-
theme: SettingsTheme,
|
|
48
|
-
tui: TUI,
|
|
49
|
-
currentValue: string,
|
|
50
|
-
wizCtx: WizardStepContext,
|
|
51
|
-
onUrl: (url: string) => void,
|
|
52
|
-
) {
|
|
53
|
-
this.theme = theme;
|
|
54
|
-
this.tui = tui;
|
|
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;
|
|
71
|
-
|
|
72
|
-
this.timer = setInterval(() => {
|
|
73
|
-
this.frame = (this.frame + 1) % SPINNER_FRAMES.length;
|
|
74
|
-
this.tui.requestRender();
|
|
75
|
-
}, 80);
|
|
76
|
-
|
|
77
|
-
checkApertureHealth(url).then((res) => {
|
|
78
|
-
if (this.timer) clearInterval(this.timer);
|
|
79
|
-
this.timer = null;
|
|
80
|
-
|
|
81
|
-
if (res.ok) {
|
|
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();
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
render(width: number): string[] {
|
|
96
|
-
const lines: string[] = [];
|
|
97
|
-
|
|
98
|
-
lines.push(
|
|
99
|
-
this.theme.hint(` Aperture base URL (e.g. ${this.placeholder}):`),
|
|
100
|
-
);
|
|
101
|
-
lines.push(` ${this.input.render(width - 4).join("")}`);
|
|
102
|
-
lines.push("");
|
|
103
|
-
|
|
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."));
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return lines;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
invalidate(): void {}
|
|
118
|
-
|
|
119
|
-
handleInput(data: string): void {
|
|
120
|
-
if (this.state === "checking") return;
|
|
121
|
-
this.state = "idle";
|
|
122
|
-
this.input.handleInput(data);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
dispose(): void {
|
|
126
|
-
if (this.timer) clearInterval(this.timer);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// ---------------------------------------------------------------------------
|
|
131
|
-
// Command registration
|
|
132
|
-
// ---------------------------------------------------------------------------
|
|
133
|
-
|
|
134
|
-
export function registerSetupCommand(
|
|
135
|
-
pi: ExtensionAPI,
|
|
136
|
-
onSync: (ctx: ExtensionContext) => void,
|
|
137
|
-
): void {
|
|
138
|
-
pi.registerCommand("aperture:setup", {
|
|
139
|
-
description: "Configure Tailscale Aperture integration",
|
|
140
|
-
handler: async (_args, ctx) => {
|
|
141
|
-
if (!ctx.hasUI) {
|
|
142
|
-
ctx.ui.notify(
|
|
143
|
-
"aperture:setup requires an interactive terminal",
|
|
144
|
-
"error",
|
|
145
|
-
);
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const config = configLoader.getConfig();
|
|
150
|
-
const checkGatewayProviders = config.checkGatewayModels ?? [];
|
|
151
|
-
|
|
152
|
-
const knownProviders = Array.from(
|
|
153
|
-
new Set(ctx.modelRegistry.getAll().map((model) => model.provider)),
|
|
154
|
-
).sort((a, b) => a.localeCompare(b));
|
|
155
|
-
|
|
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
|
-
});
|
|
207
|
-
},
|
|
208
|
-
);
|
|
209
|
-
|
|
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);
|
|
219
|
-
|
|
220
|
-
await configLoader.save("global", {
|
|
221
|
-
baseUrl,
|
|
222
|
-
providers,
|
|
223
|
-
checkGatewayModels,
|
|
224
|
-
});
|
|
225
|
-
onSync(ctx);
|
|
226
|
-
ctx.ui.notify(
|
|
227
|
-
`Aperture configured: ${providers.length} provider(s) via ${baseUrl}`,
|
|
228
|
-
"info",
|
|
229
|
-
);
|
|
230
|
-
},
|
|
231
|
-
});
|
|
232
|
-
}
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
2
|
-
import { configLoader } from "../lib/config";
|
|
3
|
-
import { fetchGatewayModels } from "../lib/gateway";
|
|
4
|
-
import type { Api, Model } from "../lib/types";
|
|
5
|
-
import { ApertureRuntime } from "./runtime";
|
|
6
|
-
|
|
7
|
-
vi.mock("../lib/config", () => ({
|
|
8
|
-
configLoader: {
|
|
9
|
-
getConfig: vi.fn(),
|
|
10
|
-
},
|
|
11
|
-
}));
|
|
12
|
-
|
|
13
|
-
vi.mock("../lib/gateway", () => ({
|
|
14
|
-
fetchGatewayModels: vi.fn(),
|
|
15
|
-
}));
|
|
16
|
-
|
|
17
|
-
const getConfig = vi.mocked(configLoader.getConfig);
|
|
18
|
-
const fetchModels = vi.mocked(fetchGatewayModels);
|
|
19
|
-
|
|
20
|
-
function model(provider: string, id: string): Model<Api> {
|
|
21
|
-
return { provider, id } as Model<Api>;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async function check(models: Model<Api>[]) {
|
|
25
|
-
const notify = vi.fn();
|
|
26
|
-
const runtime = new ApertureRuntime();
|
|
27
|
-
|
|
28
|
-
await runtime.checkMissingModels(
|
|
29
|
-
{
|
|
30
|
-
getModels: () => models,
|
|
31
|
-
notify,
|
|
32
|
-
},
|
|
33
|
-
"http://gateway.test",
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
return notify;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
describe("ApertureRuntime.checkMissingModels", () => {
|
|
40
|
-
beforeEach(() => {
|
|
41
|
-
getConfig.mockReturnValue({
|
|
42
|
-
baseUrl: "http://gateway.test",
|
|
43
|
-
providers: [],
|
|
44
|
-
checkGatewayModels: ["synthetic"],
|
|
45
|
-
});
|
|
46
|
-
fetchModels.mockResolvedValue([]);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test("matches gateway models by provider and id", async () => {
|
|
50
|
-
fetchModels.mockResolvedValue([{ providerId: "openrouter", id: "foo" }]);
|
|
51
|
-
|
|
52
|
-
const notify = await check([
|
|
53
|
-
model("synthetic", "foo"),
|
|
54
|
-
model("openrouter", "foo"),
|
|
55
|
-
]);
|
|
56
|
-
|
|
57
|
-
expect(notify).toHaveBeenCalledOnce();
|
|
58
|
-
expect(notify.mock.calls[0][0]).toContain("synthetic: foo");
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test("only checks configured providers", async () => {
|
|
62
|
-
getConfig.mockReturnValue({
|
|
63
|
-
baseUrl: "http://gateway.test",
|
|
64
|
-
providers: [],
|
|
65
|
-
checkGatewayModels: ["synthetic"],
|
|
66
|
-
});
|
|
67
|
-
fetchModels.mockResolvedValue([{ providerId: "synthetic", id: "foo" }]);
|
|
68
|
-
|
|
69
|
-
const notify = await check([
|
|
70
|
-
model("synthetic", "foo"),
|
|
71
|
-
model("openrouter", "missing-openrouter"),
|
|
72
|
-
]);
|
|
73
|
-
|
|
74
|
-
expect(notify).not.toHaveBeenCalled();
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test("truncates missing models per provider", async () => {
|
|
78
|
-
getConfig.mockReturnValue({
|
|
79
|
-
baseUrl: "http://gateway.test",
|
|
80
|
-
providers: [],
|
|
81
|
-
checkGatewayModels: ["openrouter", "synthetic"],
|
|
82
|
-
});
|
|
83
|
-
fetchModels.mockResolvedValue([{ providerId: "synthetic", id: "syn-1" }]);
|
|
84
|
-
|
|
85
|
-
const notify = await check([
|
|
86
|
-
model("openrouter", "or-1"),
|
|
87
|
-
model("openrouter", "or-2"),
|
|
88
|
-
model("openrouter", "or-3"),
|
|
89
|
-
model("openrouter", "or-4"),
|
|
90
|
-
model("openrouter", "or-5"),
|
|
91
|
-
model("openrouter", "or-6"),
|
|
92
|
-
model("openrouter", "or-7"),
|
|
93
|
-
model("synthetic", "syn-1"),
|
|
94
|
-
model("synthetic", "syn-2"),
|
|
95
|
-
model("synthetic", "syn-3"),
|
|
96
|
-
]);
|
|
97
|
-
|
|
98
|
-
expect(notify).toHaveBeenCalledOnce();
|
|
99
|
-
const message = notify.mock.calls[0][0];
|
|
100
|
-
expect(message).toContain(
|
|
101
|
-
"openrouter: or-1, or-2, or-3, or-4, or-5, 2 more",
|
|
102
|
-
);
|
|
103
|
-
expect(message).not.toContain("or-6");
|
|
104
|
-
expect(message).not.toContain("or-7");
|
|
105
|
-
expect(message).toContain("synthetic: syn-2, syn-3");
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
test("does not warn when all checked provider models exist", async () => {
|
|
109
|
-
fetchModels.mockResolvedValue([
|
|
110
|
-
{ providerId: "synthetic", id: "foo" },
|
|
111
|
-
{ providerId: "synthetic", id: "bar" },
|
|
112
|
-
]);
|
|
113
|
-
|
|
114
|
-
const notify = await check([
|
|
115
|
-
model("synthetic", "foo"),
|
|
116
|
-
model("synthetic", "bar"),
|
|
117
|
-
]);
|
|
118
|
-
|
|
119
|
-
expect(notify).not.toHaveBeenCalled();
|
|
120
|
-
});
|
|
121
|
-
});
|
package/src/extension/runtime.ts
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ApertureRuntime -- core extension runtime logic.
|
|
3
|
-
*
|
|
4
|
-
* Handles provider registration, unregistration, and gateway model checking.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { getApiProvider } from "@mariozechner/pi-ai";
|
|
8
|
-
import { configLoader } from "../lib/config";
|
|
9
|
-
import { fetchGatewayModels } from "../lib/gateway";
|
|
10
|
-
import type {
|
|
11
|
-
Api,
|
|
12
|
-
AssistantMessageEventStream,
|
|
13
|
-
CheckDeps,
|
|
14
|
-
Context,
|
|
15
|
-
Model,
|
|
16
|
-
SimpleStreamOptions,
|
|
17
|
-
SyncDeps,
|
|
18
|
-
} from "../lib/types";
|
|
19
|
-
import { resolveProviderBaseUrl } from "../lib/url";
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Preserve provenance similarly to pi-synthetic so downstream providers can
|
|
23
|
-
* attribute traffic to Pi / this extension.
|
|
24
|
-
*/
|
|
25
|
-
const APERTURE_PROVENANCE_HEADERS = {
|
|
26
|
-
Referer: "https://pi.dev",
|
|
27
|
-
"X-Title": "npm:@aliou/pi-ts-aperture",
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const MAX_MISSING_MODELS_PER_PROVIDER = 5;
|
|
31
|
-
|
|
32
|
-
function resolveProviderHeaders(models: Model<Api>[]): Record<string, string> {
|
|
33
|
-
const modelHeaders = models.find((m) => m.headers)?.headers ?? {};
|
|
34
|
-
return {
|
|
35
|
-
...APERTURE_PROVENANCE_HEADERS,
|
|
36
|
-
...modelHeaders,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export class ApertureRuntime {
|
|
41
|
-
private registeredProviders = new Set<string>();
|
|
42
|
-
|
|
43
|
-
async sync(deps: SyncDeps): Promise<void> {
|
|
44
|
-
const config = configLoader.getConfig();
|
|
45
|
-
if (!config.baseUrl || config.providers.length === 0) {
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const baseUrl = resolveProviderBaseUrl(config);
|
|
50
|
-
if (!baseUrl) return;
|
|
51
|
-
|
|
52
|
-
const allModels = deps.getModels();
|
|
53
|
-
|
|
54
|
-
for (const providerName of config.providers) {
|
|
55
|
-
const providerModels = allModels.filter(
|
|
56
|
-
(m) => m.provider === providerName,
|
|
57
|
-
);
|
|
58
|
-
if (providerModels.length === 0) continue;
|
|
59
|
-
|
|
60
|
-
const api = providerModels[0].api ?? "openai-completions";
|
|
61
|
-
const builtIn = getApiProvider(api);
|
|
62
|
-
|
|
63
|
-
deps.registerProvider(providerName, {
|
|
64
|
-
baseUrl,
|
|
65
|
-
apiKey: "-",
|
|
66
|
-
headers: resolveProviderHeaders(providerModels),
|
|
67
|
-
api,
|
|
68
|
-
streamSimple: builtIn
|
|
69
|
-
? (
|
|
70
|
-
model: Model<Api>,
|
|
71
|
-
context: Context,
|
|
72
|
-
options?: SimpleStreamOptions,
|
|
73
|
-
): AssistantMessageEventStream =>
|
|
74
|
-
builtIn.streamSimple(model, context, {
|
|
75
|
-
...options,
|
|
76
|
-
headers: {
|
|
77
|
-
...options?.headers,
|
|
78
|
-
"x-session-id": options?.sessionId ?? "",
|
|
79
|
-
},
|
|
80
|
-
})
|
|
81
|
-
: undefined,
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
this.registeredProviders.add(providerName);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async checkMissingModels(deps: CheckDeps, gatewayUrl: string): Promise<void> {
|
|
89
|
-
const config = configLoader.getConfig();
|
|
90
|
-
if (config.checkGatewayModels.length === 0) return;
|
|
91
|
-
|
|
92
|
-
const gatewayModels = await fetchGatewayModels(gatewayUrl);
|
|
93
|
-
if (gatewayModels.length === 0) return;
|
|
94
|
-
|
|
95
|
-
const allModels = deps.getModels();
|
|
96
|
-
const checkedProviders = new Set(config.checkGatewayModels);
|
|
97
|
-
const gatewayModelKeys = new Set(
|
|
98
|
-
gatewayModels.map((m) => `${m.providerId}:${m.id}`),
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
const routedModels = allModels.filter((m) =>
|
|
102
|
-
checkedProviders.has(m.provider),
|
|
103
|
-
);
|
|
104
|
-
const missingModels = routedModels.filter(
|
|
105
|
-
(m) => !gatewayModelKeys.has(`${m.provider}:${m.id}`),
|
|
106
|
-
);
|
|
107
|
-
|
|
108
|
-
if (missingModels.length > 0) {
|
|
109
|
-
const missingByProvider = new Map<string, Model<Api>[]>();
|
|
110
|
-
for (const model of missingModels) {
|
|
111
|
-
const providerModels = missingByProvider.get(model.provider) ?? [];
|
|
112
|
-
providerModels.push(model);
|
|
113
|
-
missingByProvider.set(model.provider, providerModels);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const summary = Array.from(missingByProvider.entries())
|
|
117
|
-
.map(([provider, models]) => {
|
|
118
|
-
const shownModels = models
|
|
119
|
-
.slice(0, MAX_MISSING_MODELS_PER_PROVIDER)
|
|
120
|
-
.map((m) => m.id);
|
|
121
|
-
const remainingCount = models.length - shownModels.length;
|
|
122
|
-
const more = remainingCount > 0 ? `, ${remainingCount} more` : "";
|
|
123
|
-
return `${provider}: ${shownModels.join(", ")}${more}`;
|
|
124
|
-
})
|
|
125
|
-
.join("; ");
|
|
126
|
-
|
|
127
|
-
deps.notify(
|
|
128
|
-
`[aperture] models not available on gateway: ${summary}. Add them to the gateway configuration.`,
|
|
129
|
-
"warning",
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Returns providers that should be unregistered based on config changes.
|
|
136
|
-
* Compares previous providers with new ones.
|
|
137
|
-
*/
|
|
138
|
-
getProvidersToUnregister(
|
|
139
|
-
prevProviders: string[],
|
|
140
|
-
nextProviders: string[],
|
|
141
|
-
): string[] {
|
|
142
|
-
return prevProviders.filter((p) => !nextProviders.includes(p));
|
|
143
|
-
}
|
|
144
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pi extension for Tailscale Aperture integration.
|
|
3
|
-
*
|
|
4
|
-
* Entry point orchestration:
|
|
5
|
-
* - Load config
|
|
6
|
-
* - Register session_start hook for provider registration
|
|
7
|
-
* - Register user commands
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import type {
|
|
11
|
-
ExtensionAPI,
|
|
12
|
-
ExtensionContext,
|
|
13
|
-
} from "@mariozechner/pi-coding-agent";
|
|
14
|
-
import { registerApertureSettings } from "./commands/settings";
|
|
15
|
-
import { registerSetupCommand } from "./commands/setup";
|
|
16
|
-
import { ApertureRuntime } from "./extension/runtime";
|
|
17
|
-
import { configLoader } from "./lib/config";
|
|
18
|
-
import { resolveGatewayUrl } from "./lib/url";
|
|
19
|
-
|
|
20
|
-
export default async function (pi: ExtensionAPI): Promise<void> {
|
|
21
|
-
await configLoader.load();
|
|
22
|
-
|
|
23
|
-
const runtime = new ApertureRuntime();
|
|
24
|
-
let lastRegisteredProviders: string[] = [
|
|
25
|
-
...configLoader.getConfig().providers,
|
|
26
|
-
];
|
|
27
|
-
|
|
28
|
-
// Sync function used by commands after config changes
|
|
29
|
-
const onSync = (ctx: ExtensionContext): void => {
|
|
30
|
-
const config = configLoader.getConfig();
|
|
31
|
-
|
|
32
|
-
// Unregister providers that were removed from config
|
|
33
|
-
const prevProviders = lastRegisteredProviders;
|
|
34
|
-
const nextProviders = config.providers;
|
|
35
|
-
const toRemove = runtime.getProvidersToUnregister(
|
|
36
|
-
prevProviders,
|
|
37
|
-
nextProviders,
|
|
38
|
-
);
|
|
39
|
-
for (const provider of toRemove) {
|
|
40
|
-
pi.unregisterProvider(provider);
|
|
41
|
-
ctx.ui.notify(
|
|
42
|
-
`[aperture] unregistered ${provider}. Run /reload to use the native provider.`,
|
|
43
|
-
"info",
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Re-register providers
|
|
48
|
-
void runtime
|
|
49
|
-
.sync({
|
|
50
|
-
registerProvider: pi.registerProvider.bind(pi),
|
|
51
|
-
getModels: () => ctx.modelRegistry.getAll(),
|
|
52
|
-
})
|
|
53
|
-
.then(() => {
|
|
54
|
-
// Refresh active model if it's from a registered provider
|
|
55
|
-
if (
|
|
56
|
-
ctx.model &&
|
|
57
|
-
ctx.modelRegistry.find(ctx.model.provider, ctx.model.id)
|
|
58
|
-
) {
|
|
59
|
-
const updated = ctx.modelRegistry.find(
|
|
60
|
-
ctx.model.provider,
|
|
61
|
-
ctx.model.id,
|
|
62
|
-
);
|
|
63
|
-
if (updated && config.providers.includes(ctx.model.provider)) {
|
|
64
|
-
void pi.setModel(updated);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
// Check for missing models on gateway if configured
|
|
70
|
-
if (config.checkGatewayModels.length > 0) {
|
|
71
|
-
const gatewayUrl = resolveGatewayUrl(config);
|
|
72
|
-
if (gatewayUrl) {
|
|
73
|
-
void runtime.checkMissingModels(
|
|
74
|
-
{
|
|
75
|
-
getModels: () => ctx.modelRegistry.getAll(),
|
|
76
|
-
notify: (msg, type) => ctx.ui.notify(msg, type),
|
|
77
|
-
},
|
|
78
|
-
gatewayUrl,
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
lastRegisteredProviders = [...nextProviders];
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
// Register providers at session start (for new sessions)
|
|
87
|
-
pi.on("session_start", (_event, ctx) => {
|
|
88
|
-
lastRegisteredProviders = [...configLoader.getConfig().providers];
|
|
89
|
-
void runtime.sync({
|
|
90
|
-
registerProvider: pi.registerProvider.bind(pi),
|
|
91
|
-
getModels: () => ctx.modelRegistry.getAll(),
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
registerSetupCommand(pi, onSync);
|
|
96
|
-
registerApertureSettings(pi, onSync);
|
|
97
|
-
}
|
package/src/lib/config.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Configuration schema and loader for the Aperture extension.
|
|
3
|
-
*
|
|
4
|
-
* ApertureConfig is the user-facing schema (all fields optional).
|
|
5
|
-
* ResolvedConfig is the internal schema (all fields required, defaults applied).
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { ConfigLoader } from "@aliou/pi-utils-settings";
|
|
9
|
-
|
|
10
|
-
export interface ApertureConfig {
|
|
11
|
-
baseUrl?: string;
|
|
12
|
-
providers?: string[];
|
|
13
|
-
checkGatewayModels?: string[];
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface ResolvedConfig {
|
|
17
|
-
baseUrl: string;
|
|
18
|
-
providers: string[];
|
|
19
|
-
checkGatewayModels: string[];
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const DEFAULT_CONFIG: ResolvedConfig = {
|
|
23
|
-
baseUrl: "",
|
|
24
|
-
providers: [],
|
|
25
|
-
checkGatewayModels: [],
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
export const configLoader = new ConfigLoader<ApertureConfig, ResolvedConfig>(
|
|
29
|
-
"aperture",
|
|
30
|
-
DEFAULT_CONFIG,
|
|
31
|
-
{ scopes: ["global"] },
|
|
32
|
-
);
|
package/src/lib/gateway.ts
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Gateway health and model checking.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export interface HealthCheckResult {
|
|
6
|
-
ok: boolean;
|
|
7
|
-
error?: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export async function checkApertureHealth(
|
|
11
|
-
baseUrl: string,
|
|
12
|
-
): Promise<HealthCheckResult> {
|
|
13
|
-
const url = `${baseUrl.replace(/\/+$/, "")}/v1/models`;
|
|
14
|
-
try {
|
|
15
|
-
const res = await fetch(url, {
|
|
16
|
-
method: "GET",
|
|
17
|
-
signal: AbortSignal.timeout(5000),
|
|
18
|
-
});
|
|
19
|
-
if (!res.ok) {
|
|
20
|
-
return { ok: false, error: `HTTP ${res.status} ${res.statusText}` };
|
|
21
|
-
}
|
|
22
|
-
return { ok: true };
|
|
23
|
-
} catch (e: unknown) {
|
|
24
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
25
|
-
return { ok: false, error: msg };
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface GatewayModel {
|
|
30
|
-
id: string;
|
|
31
|
-
providerId: string;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export async function fetchGatewayModels(
|
|
35
|
-
baseUrl: string,
|
|
36
|
-
): Promise<GatewayModel[]> {
|
|
37
|
-
const url = `${baseUrl.replace(/\/+$/, "")}/v1/models`;
|
|
38
|
-
try {
|
|
39
|
-
const res = await fetch(url, {
|
|
40
|
-
method: "GET",
|
|
41
|
-
signal: AbortSignal.timeout(5000),
|
|
42
|
-
});
|
|
43
|
-
if (!res.ok) return [];
|
|
44
|
-
const body = (await res.json()) as {
|
|
45
|
-
data?: {
|
|
46
|
-
id: string;
|
|
47
|
-
metadata?: { provider?: { id?: string } };
|
|
48
|
-
}[];
|
|
49
|
-
};
|
|
50
|
-
return (
|
|
51
|
-
body.data
|
|
52
|
-
?.map((m) => ({
|
|
53
|
-
id: m.id,
|
|
54
|
-
providerId: m.metadata?.provider?.id ?? "",
|
|
55
|
-
}))
|
|
56
|
-
.filter((m) => m.providerId.length > 0) ?? []
|
|
57
|
-
);
|
|
58
|
-
} catch {
|
|
59
|
-
return [];
|
|
60
|
-
}
|
|
61
|
-
}
|