@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 +5 -5
- package/src/index.ts +23 -11
- package/src/lib/health.ts +15 -0
- package/src/providers/aperture.ts +27 -82
- 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.3.
|
|
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": "
|
|
36
|
-
"@mariozechner/pi-coding-agent": "
|
|
37
|
-
"@mariozechner/pi-tui": "
|
|
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.
|
|
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
|
|
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
|
-
|
|
48
|
-
|
|
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 {
|
|
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
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* -
|
|
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
|
-
|
|
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
|
-
|
|
71
|
+
if (existingModels.length === 0) continue;
|
|
105
72
|
|
|
106
73
|
pi.registerProvider(provider, {
|
|
107
74
|
baseUrl,
|
|
108
75
|
apiKey: "-",
|
|
109
|
-
headers: resolveProviderHeaders(
|
|
110
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
83
|
+
const gatewayModelIds = gatewayUrl
|
|
84
|
+
? await fetchGatewayModelIds(gatewayUrl)
|
|
85
|
+
: [];
|
|
129
86
|
|
|
130
|
-
let
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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. */
|
package/src/lib/aperture-api.ts
DELETED
|
@@ -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
|
-
}
|