@aliou/pi-ts-aperture 0.3.1 → 0.3.2

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 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.3.1",
4
+ "version": "0.3.2",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "private": false,
@@ -32,9 +32,9 @@
32
32
  "@aliou/pi-utils-settings": "^0.10.0"
33
33
  },
34
34
  "peerDependencies": {
35
- "@mariozechner/pi-ai": ">=0.55.3",
36
- "@mariozechner/pi-coding-agent": ">=0.55.3",
37
- "@mariozechner/pi-tui": ">=0.55.3"
35
+ "@mariozechner/pi-ai": "0.61.0",
36
+ "@mariozechner/pi-coding-agent": "0.61.0",
37
+ "@mariozechner/pi-tui": "0.61.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.55.3",
54
+ "@mariozechner/pi-coding-agent": "0.61.0",
55
55
  "@sinclair/typebox": "^0.34.48",
56
56
  "@types/node": "^25.0.10",
57
57
  "@vitest/coverage-v8": "^4.0.18",
package/src/index.ts CHANGED
@@ -3,7 +3,6 @@
3
3
  *
4
4
  * Keeps the entry point focused on orchestration:
5
5
  * - load config
6
- * - bootstrap provider/model visibility
7
6
  * - register lifecycle hooks
8
7
  * - register user commands
9
8
  */
@@ -15,18 +14,26 @@ import type {
15
14
  import { registerApertureSettings } from "./commands/settings";
16
15
  import { registerSetupCommand } from "./commands/setup";
17
16
  import { configLoader } from "./config";
18
- import {
19
- applyAperture,
20
- bootstrapProvidersFromAperture,
21
- refreshActiveModel,
22
- resetApertureModelsCache,
23
- } from "./providers/aperture";
17
+ import { applyAperture, refreshActiveModel } from "./providers/aperture";
24
18
 
25
19
  function registerApertureLifecycleHook(pi: ExtensionAPI): void {
20
+ const warnedModels = new Set<string>();
21
+
26
22
  pi.on("before_agent_start", async (_event, ctx) => {
27
23
  if (!ctx?.modelRegistry) return;
28
24
 
29
- const overriddenProviders = await applyAperture(pi, ctx.modelRegistry);
25
+ const { providers: overriddenProviders, missingModels } =
26
+ await applyAperture(pi, ctx.modelRegistry);
27
+
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",
34
+ );
35
+ }
36
+
30
37
  if (!ctx.model || !overriddenProviders.includes(ctx.model.provider)) return;
31
38
 
32
39
  await refreshActiveModel(pi, ctx);
@@ -44,8 +51,14 @@ function createConfigChangeHandler(
44
51
  (provider) => !providers.includes(provider),
45
52
  );
46
53
 
47
- resetApertureModelsCache();
48
- void applyAperture(pi, ctx.modelRegistry);
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
+ });
49
62
  lastRegisteredProviders = [...providers];
50
63
 
51
64
  if (ctx.model && providers.includes(ctx.model.provider)) {
@@ -66,7 +79,6 @@ function createConfigChangeHandler(
66
79
 
67
80
  export default async function (pi: ExtensionAPI): Promise<void> {
68
81
  await configLoader.load();
69
- await bootstrapProvidersFromAperture(pi);
70
82
 
71
83
  registerApertureLifecycleHook(pi);
72
84
 
package/src/lib/health.ts CHANGED
@@ -28,3 +28,18 @@ export async function checkApertureHealth(
28
28
  return { ok: false, error: msg };
29
29
  }
30
30
  }
31
+
32
+ export async function fetchGatewayModelIds(baseUrl: string): Promise<string[]> {
33
+ const url = `${baseUrl.replace(/\/+$/, "")}/v1/models`;
34
+ try {
35
+ const res = await fetch(url, {
36
+ method: "GET",
37
+ signal: AbortSignal.timeout(5000),
38
+ });
39
+ if (!res.ok) return [];
40
+ const body = (await res.json()) as { data?: { id: string }[] };
41
+ return body.data?.map((m) => m.id) ?? [];
42
+ } catch {
43
+ return [];
44
+ }
45
+ }
@@ -4,13 +4,7 @@ import type {
4
4
  ProviderModelConfig,
5
5
  } from "@mariozechner/pi-coding-agent";
6
6
  import { configLoader } from "../config";
7
- import { fetchApertureProviderModels } from "../lib/aperture-api";
8
- import {
9
- clearProviderModelsCache,
10
- getProviderModelsCache,
11
- setProviderModelsCache,
12
- } from "../state/provider-model-cache";
13
- import { mergeModels, toModelConfig } from "./model-config";
7
+ import { fetchGatewayModelIds } from "../lib/health";
14
8
 
15
9
  /**
16
10
  * Preserve provenance similarly to pi-synthetic so downstream providers can
@@ -21,12 +15,6 @@ const APERTURE_PROVENANCE_HEADERS = {
21
15
  "X-Title": "npm:@aliou/pi-ts-aperture",
22
16
  };
23
17
 
24
- /**
25
- * Providers for which we bootstrap models at startup (before first turn)
26
- * to make CLI model selection deterministic.
27
- */
28
- const BOOTSTRAP_DISCOVERY_PROVIDERS = new Set(["openrouter"]);
29
-
30
18
  /** Returns configured gateway URL without trailing slash. */
31
19
  export function resolveGatewayUrl(): string | null {
32
20
  const { baseUrl, providers } = configLoader.getConfig();
@@ -57,100 +45,57 @@ function resolveProviderHeaders(
57
45
  };
58
46
  }
59
47
 
60
- async function getOrLoadProviderModelsCache(
61
- gatewayUrl: string,
62
- providers: string[],
63
- ): Promise<Map<string, string[]>> {
64
- const current = getProviderModelsCache();
65
- if (current) return current;
66
-
67
- const loaded = await fetchApertureProviderModels(gatewayUrl, providers);
68
- setProviderModelsCache(loaded);
69
- return loaded;
70
- }
71
-
72
- export function resetApertureModelsCache(): void {
73
- clearProviderModelsCache();
74
- }
75
-
76
48
  /**
77
- * Apply Aperture override to configured providers:
78
- * - provider baseUrl -> aperture /v1 endpoint
79
- * - apiKey -> dummy token (Aperture injects real key server-side)
80
- * - headers -> provenance + provider/model headers
49
+ * Apply Aperture override to configured providers.
50
+ *
51
+ * Only patches baseUrl, apiKey, and headers. Models are left exactly as
52
+ * registered by Pi built-ins or other extensions -- Aperture never touches
53
+ * model definitions.
54
+ *
55
+ * Providers with no models in the registry are skipped (nothing to reroute).
81
56
  */
82
57
  export async function applyAperture(
83
58
  pi: ExtensionAPI,
84
59
  registry: ExtensionContext["modelRegistry"],
85
- ): Promise<string[]> {
60
+ ): Promise<{ providers: string[]; missingModels: string[] }> {
86
61
  const baseUrl = resolveApertureProviderBaseUrl();
87
- const gatewayUrl = resolveGatewayUrl();
88
- if (!baseUrl || !gatewayUrl) return [];
62
+ if (!baseUrl) return { providers: [], missingModels: [] };
89
63
 
90
64
  const { providers } = configLoader.getConfig();
91
65
 
92
- let modelCache: Map<string, string[]>;
93
- try {
94
- modelCache = await getOrLoadProviderModelsCache(gatewayUrl, providers);
95
- } catch {
96
- modelCache = new Map();
97
- }
98
-
99
66
  for (const provider of providers) {
100
67
  const existingModels = registry
101
68
  .getAll()
102
69
  .filter((m) => m.provider === provider) as ProviderModelConfig[];
103
70
 
104
- const models = mergeModels(existingModels, modelCache.get(provider));
71
+ if (existingModels.length === 0) continue;
105
72
 
106
73
  pi.registerProvider(provider, {
107
74
  baseUrl,
108
75
  apiKey: "-",
109
- headers: resolveProviderHeaders(models),
110
- ...(models.length > 0 && { api: models[0].api, models }),
76
+ headers: resolveProviderHeaders(existingModels),
77
+ api: existingModels[0].api,
78
+ models: existingModels,
111
79
  });
112
80
  }
113
81
 
114
- return providers;
115
- }
116
-
117
- /**
118
- * Pre-register selected providers from Aperture model discovery so CLI model
119
- * resolution works even when a model is not present in Pi built-ins.
120
- */
121
- export async function bootstrapProvidersFromAperture(
122
- pi: ExtensionAPI,
123
- ): Promise<void> {
124
- const baseUrl = resolveApertureProviderBaseUrl();
125
82
  const gatewayUrl = resolveGatewayUrl();
126
- if (!baseUrl || !gatewayUrl) return;
127
-
128
- const { providers } = configLoader.getConfig();
83
+ const gatewayModelIds = gatewayUrl
84
+ ? await fetchGatewayModelIds(gatewayUrl)
85
+ : [];
129
86
 
130
- let modelCache: Map<string, string[]>;
131
- try {
132
- modelCache = await fetchApertureProviderModels(gatewayUrl, providers);
133
- setProviderModelsCache(modelCache);
134
- } catch {
135
- return;
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
+ );
136
96
  }
137
97
 
138
- for (const provider of providers) {
139
- if (!BOOTSTRAP_DISCOVERY_PROVIDERS.has(provider)) continue;
140
-
141
- const modelIds = modelCache.get(provider) ?? [];
142
- if (modelIds.length === 0) continue;
143
-
144
- const models = modelIds.map((id) => toModelConfig(id));
145
-
146
- pi.registerProvider(provider, {
147
- baseUrl,
148
- apiKey: "-",
149
- api: "openai-completions",
150
- headers: resolveProviderHeaders(models),
151
- models,
152
- });
153
- }
98
+ return { providers, missingModels };
154
99
  }
155
100
 
156
101
  /** Re-resolve and set current model after provider registry updates. */
@@ -1,32 +0,0 @@
1
- export interface ApertureProviderInfo {
2
- id?: string;
3
- models?: string[];
4
- }
5
-
6
- /**
7
- * Fetch provider -> model list mapping from Aperture and keep only selected
8
- * providers configured by the user.
9
- */
10
- export async function fetchApertureProviderModels(
11
- gatewayUrl: string,
12
- providers: string[],
13
- ): Promise<Map<string, string[]>> {
14
- const response = await fetch(`${gatewayUrl}/api/providers`, {
15
- signal: AbortSignal.timeout(4000),
16
- });
17
-
18
- if (!response.ok) {
19
- return new Map();
20
- }
21
-
22
- const data = (await response.json()) as ApertureProviderInfo[];
23
- const selectedProviders = new Set(providers);
24
- const modelsByProvider = new Map<string, string[]>();
25
-
26
- for (const provider of data) {
27
- if (!provider.id || !selectedProviders.has(provider.id)) continue;
28
- modelsByProvider.set(provider.id, provider.models ?? []);
29
- }
30
-
31
- return modelsByProvider;
32
- }
@@ -1,54 +0,0 @@
1
- import type { ProviderModelConfig } from "@mariozechner/pi-coding-agent";
2
-
3
- const DEFAULT_COST = {
4
- input: 0,
5
- output: 0,
6
- cacheRead: 0,
7
- cacheWrite: 0,
8
- };
9
-
10
- /**
11
- * Build a ProviderModelConfig for Aperture-discovered model IDs.
12
- *
13
- * When a template model is available (same provider), preserve its API/compat
14
- * shape so behavior stays consistent after rerouting.
15
- */
16
- export function toModelConfig(
17
- id: string,
18
- template?: ProviderModelConfig,
19
- ): ProviderModelConfig {
20
- return {
21
- id,
22
- name: template?.name ?? id,
23
- api: template?.api ?? "openai-completions",
24
- reasoning: template?.reasoning ?? false,
25
- input: template?.input ?? ["text"],
26
- cost: template?.cost ?? DEFAULT_COST,
27
- contextWindow: template?.contextWindow ?? 128000,
28
- maxTokens: template?.maxTokens ?? 16384,
29
- headers: template?.headers,
30
- compat: template?.compat,
31
- };
32
- }
33
-
34
- /**
35
- * Merge known provider models with Aperture-discovered model IDs.
36
- * Existing models win; missing IDs are synthesized from template defaults.
37
- */
38
- export function mergeModels(
39
- existingModels: ProviderModelConfig[],
40
- apertureModelIds: string[] | undefined,
41
- ): ProviderModelConfig[] {
42
- if (!apertureModelIds || apertureModelIds.length === 0) return existingModels;
43
-
44
- const modelsById = new Map(existingModels.map((m) => [m.id, m]));
45
- const template = existingModels[0];
46
-
47
- for (const modelId of apertureModelIds) {
48
- if (!modelsById.has(modelId)) {
49
- modelsById.set(modelId, toModelConfig(modelId, template));
50
- }
51
- }
52
-
53
- return [...modelsById.values()];
54
- }
@@ -1,18 +0,0 @@
1
- /**
2
- * In-memory cache for Aperture provider model discovery.
3
- *
4
- * Ephemeral by design: reset on config changes and process restart.
5
- */
6
- let providerModelsCache: Map<string, string[]> | null = null;
7
-
8
- export function getProviderModelsCache(): Map<string, string[]> | null {
9
- return providerModelsCache;
10
- }
11
-
12
- export function setProviderModelsCache(models: Map<string, string[]>): void {
13
- providerModelsCache = models;
14
- }
15
-
16
- export function clearProviderModelsCache(): void {
17
- providerModelsCache = null;
18
- }