@aliou/pi-ts-aperture 0.3.2 → 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 +87 -25
- package/src/providers/aperture.ts +36 -69
package/src/index.ts
CHANGED
|
@@ -14,54 +14,114 @@ import type {
|
|
|
14
14
|
import { registerApertureSettings } from "./commands/settings";
|
|
15
15
|
import { registerSetupCommand } from "./commands/setup";
|
|
16
16
|
import { configLoader } from "./config";
|
|
17
|
-
import {
|
|
17
|
+
import { planConfigChange, resolveProviderBaseUrl } from "./core";
|
|
18
|
+
import {
|
|
19
|
+
applyAperture,
|
|
20
|
+
checkGatewayModels,
|
|
21
|
+
refreshActiveModel,
|
|
22
|
+
} from "./providers/aperture";
|
|
18
23
|
|
|
19
|
-
function
|
|
20
|
-
|
|
24
|
+
function notifyMissingModelsOnce(
|
|
25
|
+
ctx: ExtensionContext,
|
|
26
|
+
missingModels: string[],
|
|
27
|
+
warnedModels: Set<string>,
|
|
28
|
+
): void {
|
|
29
|
+
const newMissing = missingModels.filter((id) => !warnedModels.has(id));
|
|
30
|
+
if (newMissing.length > 0) {
|
|
31
|
+
for (const id of newMissing) warnedModels.add(id);
|
|
32
|
+
ctx.ui.notify(
|
|
33
|
+
`[aperture] models not available on gateway: ${newMissing.join(", ")}. Add them to the gateway configuration.`,
|
|
34
|
+
"warning",
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
21
38
|
|
|
39
|
+
function registerApertureLifecycleHook(
|
|
40
|
+
pi: ExtensionAPI,
|
|
41
|
+
warnedModels: Set<string>,
|
|
42
|
+
): void {
|
|
22
43
|
pi.on("before_agent_start", async (_event, ctx) => {
|
|
23
44
|
if (!ctx?.modelRegistry) return;
|
|
24
45
|
|
|
25
|
-
const { providers: overriddenProviders,
|
|
26
|
-
|
|
46
|
+
const { providers: overriddenProviders, gatewayUrl } = await applyAperture(
|
|
47
|
+
pi,
|
|
48
|
+
ctx.modelRegistry,
|
|
49
|
+
);
|
|
27
50
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
51
|
+
if (
|
|
52
|
+
ctx.model &&
|
|
53
|
+
overriddenProviders.includes(ctx.model.provider) &&
|
|
54
|
+
gatewayUrl !== null &&
|
|
55
|
+
configLoader.getConfig().checkGatewayModels.includes(ctx.model.provider)
|
|
56
|
+
) {
|
|
57
|
+
const { missingModels } = await checkGatewayModels(
|
|
58
|
+
gatewayUrl,
|
|
59
|
+
ctx.modelRegistry,
|
|
34
60
|
);
|
|
61
|
+
notifyMissingModelsOnce(ctx, missingModels, warnedModels);
|
|
35
62
|
}
|
|
36
63
|
|
|
37
64
|
if (!ctx.model || !overriddenProviders.includes(ctx.model.provider)) return;
|
|
38
65
|
|
|
39
66
|
await refreshActiveModel(pi, ctx);
|
|
40
67
|
});
|
|
68
|
+
|
|
69
|
+
// Also check when user switches to a model whose provider uses aperture
|
|
70
|
+
pi.on("model_select", async (_event, ctx) => {
|
|
71
|
+
if (!ctx?.model) return;
|
|
72
|
+
|
|
73
|
+
const config = configLoader.getConfig();
|
|
74
|
+
if (!config.providers.includes(ctx.model.provider)) return;
|
|
75
|
+
|
|
76
|
+
const gatewayUrl = resolveProviderBaseUrl(config)?.replace("/v1", "");
|
|
77
|
+
if (!gatewayUrl) return;
|
|
78
|
+
|
|
79
|
+
if (config.checkGatewayModels.includes(ctx.model.provider)) {
|
|
80
|
+
const { missingModels } = await checkGatewayModels(
|
|
81
|
+
gatewayUrl,
|
|
82
|
+
ctx.modelRegistry,
|
|
83
|
+
);
|
|
84
|
+
notifyMissingModelsOnce(ctx, missingModels, warnedModels);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
41
87
|
}
|
|
42
88
|
|
|
43
89
|
function createConfigChangeHandler(
|
|
44
90
|
pi: ExtensionAPI,
|
|
91
|
+
warnedModels: Set<string>,
|
|
45
92
|
): (ctx: ExtensionContext) => void {
|
|
46
93
|
let lastRegisteredProviders = [...configLoader.getConfig().providers];
|
|
47
94
|
|
|
48
95
|
return (ctx: ExtensionContext) => {
|
|
49
96
|
const { providers } = configLoader.getConfig();
|
|
50
|
-
|
|
51
|
-
|
|
97
|
+
|
|
98
|
+
const plan = planConfigChange(
|
|
99
|
+
lastRegisteredProviders,
|
|
100
|
+
providers,
|
|
101
|
+
ctx.model?.provider,
|
|
52
102
|
);
|
|
53
103
|
|
|
54
|
-
void applyAperture(pi, ctx.modelRegistry).then(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
104
|
+
void applyAperture(pi, ctx.modelRegistry).then(
|
|
105
|
+
async ({ providers, gatewayUrl }) => {
|
|
106
|
+
if (
|
|
107
|
+
ctx.model &&
|
|
108
|
+
providers.includes(ctx.model.provider) &&
|
|
109
|
+
gatewayUrl !== null &&
|
|
110
|
+
configLoader
|
|
111
|
+
.getConfig()
|
|
112
|
+
.checkGatewayModels.includes(ctx.model.provider)
|
|
113
|
+
) {
|
|
114
|
+
const { missingModels } = await checkGatewayModels(
|
|
115
|
+
gatewayUrl,
|
|
116
|
+
ctx.modelRegistry,
|
|
117
|
+
);
|
|
118
|
+
notifyMissingModelsOnce(ctx, missingModels, warnedModels);
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
);
|
|
62
122
|
lastRegisteredProviders = [...providers];
|
|
63
123
|
|
|
64
|
-
if (
|
|
124
|
+
if (plan.shouldRefreshModel) {
|
|
65
125
|
void refreshActiveModel(pi, ctx).then((updated) => {
|
|
66
126
|
if (!updated) return;
|
|
67
127
|
ctx.ui.notify(
|
|
@@ -71,7 +131,7 @@ function createConfigChangeHandler(
|
|
|
71
131
|
});
|
|
72
132
|
}
|
|
73
133
|
|
|
74
|
-
for (const provider of removedProviders) {
|
|
134
|
+
for (const provider of plan.removedProviders) {
|
|
75
135
|
pi.unregisterProvider(provider);
|
|
76
136
|
}
|
|
77
137
|
};
|
|
@@ -80,9 +140,11 @@ function createConfigChangeHandler(
|
|
|
80
140
|
export default async function (pi: ExtensionAPI): Promise<void> {
|
|
81
141
|
await configLoader.load();
|
|
82
142
|
|
|
83
|
-
|
|
143
|
+
const warnedModels = new Set<string>();
|
|
144
|
+
|
|
145
|
+
registerApertureLifecycleHook(pi, warnedModels);
|
|
84
146
|
|
|
85
|
-
const onConfigChange = createConfigChangeHandler(pi);
|
|
147
|
+
const onConfigChange = createConfigChangeHandler(pi, warnedModels);
|
|
86
148
|
registerSetupCommand(pi, onConfigChange);
|
|
87
149
|
registerApertureSettings(pi, onConfigChange);
|
|
88
150
|
}
|
|
@@ -1,49 +1,16 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
ExtensionAPI,
|
|
3
3
|
ExtensionContext,
|
|
4
|
-
ProviderModelConfig,
|
|
5
4
|
} from "@mariozechner/pi-coding-agent";
|
|
6
5
|
import { configLoader } from "../config";
|
|
6
|
+
import {
|
|
7
|
+
buildApplyPlan,
|
|
8
|
+
resolveGatewayUrl,
|
|
9
|
+
resolveProviderBaseUrl,
|
|
10
|
+
} from "../core";
|
|
7
11
|
import { fetchGatewayModelIds } from "../lib/health";
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
* Preserve provenance similarly to pi-synthetic so downstream providers can
|
|
11
|
-
* attribute traffic to Pi / this extension.
|
|
12
|
-
*/
|
|
13
|
-
const APERTURE_PROVENANCE_HEADERS = {
|
|
14
|
-
Referer: "https://pi.dev",
|
|
15
|
-
"X-Title": "npm:@aliou/pi-ts-aperture",
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
/** Returns configured gateway URL without trailing slash. */
|
|
19
|
-
export function resolveGatewayUrl(): string | null {
|
|
20
|
-
const { baseUrl, providers } = configLoader.getConfig();
|
|
21
|
-
if (!baseUrl || providers.length === 0) return null;
|
|
22
|
-
return baseUrl.replace(/\/+$/, "");
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Returns the Aperture provider base URL used for provider registration.
|
|
27
|
-
*
|
|
28
|
-
* Aperture exposes multiple protocol paths (OpenAI, Anthropic, Gemini, ...).
|
|
29
|
-
* For this extension we route through the OpenAI-compatible `/v1` surface that
|
|
30
|
-
* Pi providers use (`openai-completions` API).
|
|
31
|
-
*/
|
|
32
|
-
export function resolveApertureProviderBaseUrl(): string | null {
|
|
33
|
-
const gateway = resolveGatewayUrl();
|
|
34
|
-
if (!gateway) return null;
|
|
35
|
-
return `${gateway}/v1`;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function resolveProviderHeaders(
|
|
39
|
-
models: ProviderModelConfig[],
|
|
40
|
-
): Record<string, string> {
|
|
41
|
-
const modelHeaders = models.find((m) => m.headers)?.headers ?? {};
|
|
42
|
-
return {
|
|
43
|
-
...APERTURE_PROVENANCE_HEADERS,
|
|
44
|
-
...modelHeaders,
|
|
45
|
-
};
|
|
46
|
-
}
|
|
13
|
+
export { resolveGatewayUrl } from "../core";
|
|
47
14
|
|
|
48
15
|
/**
|
|
49
16
|
* Apply Aperture override to configured providers.
|
|
@@ -57,45 +24,45 @@ function resolveProviderHeaders(
|
|
|
57
24
|
export async function applyAperture(
|
|
58
25
|
pi: ExtensionAPI,
|
|
59
26
|
registry: ExtensionContext["modelRegistry"],
|
|
60
|
-
): Promise<{ providers: string[];
|
|
61
|
-
const
|
|
62
|
-
|
|
27
|
+
): Promise<{ providers: string[]; gatewayUrl: string | null }> {
|
|
28
|
+
const config = configLoader.getConfig();
|
|
29
|
+
const baseUrl = resolveProviderBaseUrl(config);
|
|
30
|
+
if (!baseUrl) return { providers: [], gatewayUrl: null };
|
|
63
31
|
|
|
64
|
-
const
|
|
32
|
+
const gatewayUrl = resolveGatewayUrl(config);
|
|
65
33
|
|
|
66
|
-
|
|
67
|
-
const existingModels = registry
|
|
68
|
-
.getAll()
|
|
69
|
-
.filter((m) => m.provider === provider) as ProviderModelConfig[];
|
|
34
|
+
const registryModels = registry.getAll();
|
|
70
35
|
|
|
71
|
-
|
|
36
|
+
const plan = buildApplyPlan(config, registryModels, baseUrl, []);
|
|
72
37
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
38
|
+
for (const reg of plan.registrations) {
|
|
39
|
+
pi.registerProvider(reg.provider, {
|
|
40
|
+
baseUrl: reg.baseUrl,
|
|
41
|
+
apiKey: reg.apiKey,
|
|
42
|
+
headers: reg.headers,
|
|
43
|
+
api: reg.api,
|
|
44
|
+
models: reg.models,
|
|
79
45
|
});
|
|
80
46
|
}
|
|
81
47
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
? await fetchGatewayModelIds(gatewayUrl)
|
|
85
|
-
: [];
|
|
48
|
+
return { providers: config.providers, gatewayUrl };
|
|
49
|
+
}
|
|
86
50
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
51
|
+
/**
|
|
52
|
+
* Fetch gateway models and return missing ones relative to the plan.
|
|
53
|
+
*/
|
|
54
|
+
export async function checkGatewayModels(
|
|
55
|
+
gatewayUrl: string,
|
|
56
|
+
registry: ExtensionContext["modelRegistry"],
|
|
57
|
+
): Promise<{ missingModels: string[] }> {
|
|
58
|
+
const config = configLoader.getConfig();
|
|
59
|
+
const baseUrl = resolveProviderBaseUrl(config);
|
|
60
|
+
if (!baseUrl) return { missingModels: [] };
|
|
97
61
|
|
|
98
|
-
|
|
62
|
+
const gatewayModelIds = await fetchGatewayModelIds(gatewayUrl);
|
|
63
|
+
const registryModels = registry.getAll();
|
|
64
|
+
const plan = buildApplyPlan(config, registryModels, baseUrl, gatewayModelIds);
|
|
65
|
+
return { missingModels: plan.missingModels };
|
|
99
66
|
}
|
|
100
67
|
|
|
101
68
|
/** Re-resolve and set current model after provider registry updates. */
|