@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/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 { applyAperture, refreshActiveModel } from "./providers/aperture";
17
+ import { planConfigChange, resolveProviderBaseUrl } from "./core";
18
+ import {
19
+ applyAperture,
20
+ checkGatewayModels,
21
+ refreshActiveModel,
22
+ } from "./providers/aperture";
18
23
 
19
- function registerApertureLifecycleHook(pi: ExtensionAPI): void {
20
- const warnedModels = new Set<string>();
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, missingModels } =
26
- await applyAperture(pi, ctx.modelRegistry);
46
+ const { providers: overriddenProviders, gatewayUrl } = await applyAperture(
47
+ pi,
48
+ ctx.modelRegistry,
49
+ );
27
50
 
28
- const newMissing = missingModels.filter((id) => !warnedModels.has(id));
29
- if (newMissing.length > 0) {
30
- for (const id of newMissing) warnedModels.add(id);
31
- ctx.ui.notify(
32
- `[aperture] models not available on gateway: ${newMissing.join(", ")}. Add them to the gateway configuration.`,
33
- "warning",
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
- const removedProviders = lastRegisteredProviders.filter(
51
- (provider) => !providers.includes(provider),
97
+
98
+ const plan = planConfigChange(
99
+ lastRegisteredProviders,
100
+ providers,
101
+ ctx.model?.provider,
52
102
  );
53
103
 
54
- void applyAperture(pi, ctx.modelRegistry).then(({ missingModels }) => {
55
- if (missingModels.length > 0) {
56
- ctx.ui.notify(
57
- `[aperture] models not available on gateway: ${missingModels.join(", ")}. Add them to the gateway configuration.`,
58
- "warning",
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 (ctx.model && providers.includes(ctx.model.provider)) {
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
- registerApertureLifecycleHook(pi);
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[]; missingModels: string[] }> {
61
- const baseUrl = resolveApertureProviderBaseUrl();
62
- if (!baseUrl) return { providers: [], missingModels: [] };
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 { providers } = configLoader.getConfig();
32
+ const gatewayUrl = resolveGatewayUrl(config);
65
33
 
66
- for (const provider of providers) {
67
- const existingModels = registry
68
- .getAll()
69
- .filter((m) => m.provider === provider) as ProviderModelConfig[];
34
+ const registryModels = registry.getAll();
70
35
 
71
- if (existingModels.length === 0) continue;
36
+ const plan = buildApplyPlan(config, registryModels, baseUrl, []);
72
37
 
73
- pi.registerProvider(provider, {
74
- baseUrl,
75
- apiKey: "-",
76
- headers: resolveProviderHeaders(existingModels),
77
- api: existingModels[0].api,
78
- models: existingModels,
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
- const gatewayUrl = resolveGatewayUrl();
83
- const gatewayModelIds = gatewayUrl
84
- ? await fetchGatewayModelIds(gatewayUrl)
85
- : [];
48
+ return { providers: config.providers, gatewayUrl };
49
+ }
86
50
 
87
- let missingModels: string[] = [];
88
- if (gatewayModelIds.length > 0) {
89
- const routedModelIds = registry
90
- .getAll()
91
- .filter((m) => providers.includes(m.provider))
92
- .map((m) => m.id);
93
- missingModels = routedModelIds.filter(
94
- (id) => !gatewayModelIds.includes(id),
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
- return { providers, missingModels };
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. */