@aliou/pi-ts-aperture 0.2.5 → 0.3.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 +15 -11
- package/package.json +1 -1
- package/src/index.ts +42 -90
- package/src/lib/aperture-api.ts +32 -0
- package/src/providers/aperture.ts +167 -0
- package/src/providers/model-config.ts +54 -0
- package/src/state/provider-model-cache.ts +18 -0
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Route Pi LLM providers through [Tailscale Aperture](https://tailscale.com/docs/features/aperture), a managed AI gateway on your tailnet.
|
|
4
4
|
|
|
5
|
-
Aperture handles API key injection and request routing server-side. This extension overrides
|
|
5
|
+
Aperture handles API key injection and request routing server-side. This extension overrides selected providers so requests go through your Aperture gateway instead of directly to upstream provider APIs.
|
|
6
6
|
|
|
7
7
|
## Setup
|
|
8
8
|
|
|
@@ -16,9 +16,9 @@ Then run the setup wizard:
|
|
|
16
16
|
/aperture:setup
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
This
|
|
20
|
-
1.
|
|
21
|
-
2.
|
|
19
|
+
This prompts for:
|
|
20
|
+
1. Aperture base URL (for example `ai.your-tailnet.ts.net`)
|
|
21
|
+
2. Providers to route through Aperture (fuzzy searchable, multi-select)
|
|
22
22
|
|
|
23
23
|
Configuration is saved globally to `~/.pi/agent/extensions/aperture.json`.
|
|
24
24
|
|
|
@@ -26,19 +26,23 @@ Configuration is saved globally to `~/.pi/agent/extensions/aperture.json`.
|
|
|
26
26
|
|
|
27
27
|
| Command | Description |
|
|
28
28
|
|---|---|
|
|
29
|
-
| `/aperture:setup` | Interactive wizard to configure Aperture URL and providers |
|
|
30
|
-
| `/aperture:settings` | Settings UI to update
|
|
29
|
+
| `/aperture:setup` | Interactive wizard to configure Aperture URL and routed providers |
|
|
30
|
+
| `/aperture:settings` | Settings UI to update URL and routed provider list |
|
|
31
31
|
|
|
32
32
|
## How it works
|
|
33
33
|
|
|
34
34
|
For each configured provider, the extension calls `registerProvider` with:
|
|
35
|
-
- `baseUrl` set to your Aperture URL + `/v1`
|
|
36
|
-
- `apiKey` set to `"-"` (Aperture ignores client keys, it injects its own)
|
|
37
35
|
|
|
38
|
-
|
|
36
|
+
- `baseUrl` set to your Aperture URL + `/v1` (OpenAI-compatible surface used by Pi provider configs)
|
|
37
|
+
- `apiKey` set to `"-"` (Aperture injects upstream credentials server-side)
|
|
38
|
+
- provenance headers:
|
|
39
|
+
- `Referer: https://pi.dev`
|
|
40
|
+
- `X-Title: npm:@aliou/pi-ts-aperture`
|
|
41
|
+
|
|
42
|
+
Additionally, the extension can bootstrap model IDs discovered from Aperture (`/api/providers`) for providers like OpenRouter so CLI model selection can resolve Aperture-exposed model IDs before the first prompt.
|
|
39
43
|
|
|
40
44
|
## Requirements
|
|
41
45
|
|
|
42
46
|
- A Tailscale tailnet with Aperture configured
|
|
43
|
-
- The device running Pi must be on the tailnet
|
|
44
|
-
- Use
|
|
47
|
+
- The device running Pi must be on the tailnet (or otherwise able to reach your Aperture endpoint)
|
|
48
|
+
- Use the URL/scheme that matches your deployment (`http://` or `https://`)
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,124 +1,76 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pi extension for Tailscale Aperture integration.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Keeps the entry point focused on orchestration:
|
|
5
|
+
* - load config
|
|
6
|
+
* - bootstrap provider/model visibility
|
|
7
|
+
* - register lifecycle hooks
|
|
8
|
+
* - register user commands
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
11
|
import type {
|
|
10
12
|
ExtensionAPI,
|
|
11
13
|
ExtensionContext,
|
|
12
14
|
} from "@mariozechner/pi-coding-agent";
|
|
13
|
-
import { VERSION } from "@mariozechner/pi-coding-agent";
|
|
14
15
|
import { registerApertureSettings } from "./commands/settings";
|
|
15
16
|
import { registerSetupCommand } from "./commands/setup";
|
|
16
17
|
import { configLoader } from "./config";
|
|
18
|
+
import {
|
|
19
|
+
applyAperture,
|
|
20
|
+
bootstrapProvidersFromAperture,
|
|
21
|
+
refreshActiveModel,
|
|
22
|
+
resetApertureModelsCache,
|
|
23
|
+
} from "./providers/aperture";
|
|
24
|
+
|
|
25
|
+
function registerApertureLifecycleHook(pi: ExtensionAPI): void {
|
|
26
|
+
pi.on("before_agent_start", async (_event, ctx) => {
|
|
27
|
+
if (!ctx?.modelRegistry) return;
|
|
17
28
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
*/
|
|
21
|
-
function resolveBaseUrl(): string | null {
|
|
22
|
-
const { baseUrl, providers } = configLoader.getConfig();
|
|
23
|
-
if (!baseUrl || providers.length === 0) return null;
|
|
24
|
-
return `${baseUrl.replace(/\/+$/, "")}/v1`;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Override provider registrations to route through Aperture.
|
|
29
|
-
* Preserves existing models so extensions that registered custom models
|
|
30
|
-
* before this runs don't lose them.
|
|
31
|
-
*/
|
|
32
|
-
function overrideProviders(
|
|
33
|
-
pi: ExtensionAPI,
|
|
34
|
-
registry: ExtensionContext["modelRegistry"],
|
|
35
|
-
providers: string[],
|
|
36
|
-
baseUrl: string,
|
|
37
|
-
): void {
|
|
38
|
-
for (const provider of providers) {
|
|
39
|
-
const models = registry.getAll().filter((m) => m.provider === provider);
|
|
29
|
+
const overriddenProviders = await applyAperture(pi, ctx.modelRegistry);
|
|
30
|
+
if (!ctx.model || !overriddenProviders.includes(ctx.model.provider)) return;
|
|
40
31
|
|
|
41
|
-
pi
|
|
42
|
-
|
|
43
|
-
apiKey: "-",
|
|
44
|
-
...(models.length > 0 && { api: models[0].api, models }),
|
|
45
|
-
});
|
|
46
|
-
}
|
|
32
|
+
await refreshActiveModel(pi, ctx);
|
|
33
|
+
});
|
|
47
34
|
}
|
|
48
35
|
|
|
49
|
-
|
|
50
|
-
* Apply Aperture configuration to the model registry.
|
|
51
|
-
* Returns the list of providers that were overridden, or empty if no-op.
|
|
52
|
-
*/
|
|
53
|
-
function applyAperture(
|
|
36
|
+
function createConfigChangeHandler(
|
|
54
37
|
pi: ExtensionAPI,
|
|
55
|
-
|
|
56
|
-
): string[] {
|
|
57
|
-
const url = resolveBaseUrl();
|
|
58
|
-
if (!url) return [];
|
|
59
|
-
|
|
60
|
-
const { providers } = configLoader.getConfig();
|
|
61
|
-
overrideProviders(pi, registry, providers, url);
|
|
62
|
-
return providers;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Re-resolve the active model from the registry and update it via pi.setModel().
|
|
67
|
-
* Call this after updating the registry to ensure the active model uses the new configuration.
|
|
68
|
-
* Returns true if the model was updated.
|
|
69
|
-
*/
|
|
70
|
-
function refreshActiveModel(pi: ExtensionAPI, ctx: ExtensionContext): boolean {
|
|
71
|
-
if (!ctx.model) return false;
|
|
72
|
-
|
|
73
|
-
const updated = ctx.modelRegistry.find(ctx.model.provider, ctx.model.id);
|
|
74
|
-
if (!updated) return false;
|
|
75
|
-
|
|
76
|
-
pi.setModel(updated);
|
|
77
|
-
return true;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export default async function (pi: ExtensionAPI): Promise<void> {
|
|
81
|
-
await configLoader.load();
|
|
82
|
-
|
|
38
|
+
): (ctx: ExtensionContext) => void {
|
|
83
39
|
let lastRegisteredProviders = [...configLoader.getConfig().providers];
|
|
84
40
|
|
|
85
|
-
|
|
86
|
-
pi.on("before_agent_start", async (_event, ctx) => {
|
|
87
|
-
if (!ctx?.modelRegistry) return;
|
|
88
|
-
|
|
89
|
-
const overriddenProviders = applyAperture(pi, ctx.modelRegistry);
|
|
90
|
-
|
|
91
|
-
// Re-resolve active model if it belongs to a reconfigured provider.
|
|
92
|
-
// The model was selected before before_agent_start fired, so we need to update it.
|
|
93
|
-
if (ctx.model && overriddenProviders.includes(ctx.model.provider)) {
|
|
94
|
-
refreshActiveModel(pi, ctx);
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
const onSetupComplete = (ctx: ExtensionContext) => {
|
|
41
|
+
return (ctx: ExtensionContext) => {
|
|
99
42
|
const { providers } = configLoader.getConfig();
|
|
100
|
-
const
|
|
101
|
-
(
|
|
43
|
+
const removedProviders = lastRegisteredProviders.filter(
|
|
44
|
+
(provider) => !providers.includes(provider),
|
|
102
45
|
);
|
|
103
46
|
|
|
104
|
-
|
|
47
|
+
resetApertureModelsCache();
|
|
48
|
+
void applyAperture(pi, ctx.modelRegistry);
|
|
105
49
|
lastRegisteredProviders = [...providers];
|
|
106
50
|
|
|
107
|
-
// Re-resolve active model if it belongs to a reconfigured provider.
|
|
108
51
|
if (ctx.model && providers.includes(ctx.model.provider)) {
|
|
109
|
-
|
|
52
|
+
void refreshActiveModel(pi, ctx).then((updated) => {
|
|
53
|
+
if (!updated) return;
|
|
110
54
|
ctx.ui.notify(
|
|
111
|
-
`[aperture] re-routing ${ctx.model
|
|
55
|
+
`[aperture] re-routing ${ctx.model?.id ?? "model"} through ${ctx.model?.baseUrl ?? "aperture"}`,
|
|
112
56
|
"info",
|
|
113
57
|
);
|
|
114
|
-
}
|
|
58
|
+
});
|
|
115
59
|
}
|
|
116
60
|
|
|
117
|
-
for (const
|
|
118
|
-
pi.unregisterProvider(
|
|
61
|
+
for (const provider of removedProviders) {
|
|
62
|
+
pi.unregisterProvider(provider);
|
|
119
63
|
}
|
|
120
64
|
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export default async function (pi: ExtensionAPI): Promise<void> {
|
|
68
|
+
await configLoader.load();
|
|
69
|
+
await bootstrapProvidersFromAperture(pi);
|
|
70
|
+
|
|
71
|
+
registerApertureLifecycleHook(pi);
|
|
121
72
|
|
|
122
|
-
|
|
123
|
-
|
|
73
|
+
const onConfigChange = createConfigChangeHandler(pi);
|
|
74
|
+
registerSetupCommand(pi, onConfigChange);
|
|
75
|
+
registerApertureSettings(pi, onConfigChange);
|
|
124
76
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
ProviderModelConfig,
|
|
5
|
+
} from "@mariozechner/pi-coding-agent";
|
|
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";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Preserve provenance similarly to pi-synthetic so downstream providers can
|
|
17
|
+
* attribute traffic to Pi / this extension.
|
|
18
|
+
*/
|
|
19
|
+
const APERTURE_PROVENANCE_HEADERS = {
|
|
20
|
+
Referer: "https://pi.dev",
|
|
21
|
+
"X-Title": "npm:@aliou/pi-ts-aperture",
|
|
22
|
+
};
|
|
23
|
+
|
|
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
|
+
/** Returns configured gateway URL without trailing slash. */
|
|
31
|
+
export function resolveGatewayUrl(): string | null {
|
|
32
|
+
const { baseUrl, providers } = configLoader.getConfig();
|
|
33
|
+
if (!baseUrl || providers.length === 0) return null;
|
|
34
|
+
return baseUrl.replace(/\/+$/, "");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Returns the Aperture provider base URL used for provider registration.
|
|
39
|
+
*
|
|
40
|
+
* Aperture exposes multiple protocol paths (OpenAI, Anthropic, Gemini, ...).
|
|
41
|
+
* For this extension we route through the OpenAI-compatible `/v1` surface that
|
|
42
|
+
* Pi providers use (`openai-completions` API).
|
|
43
|
+
*/
|
|
44
|
+
export function resolveApertureProviderBaseUrl(): string | null {
|
|
45
|
+
const gateway = resolveGatewayUrl();
|
|
46
|
+
if (!gateway) return null;
|
|
47
|
+
return `${gateway}/v1`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resolveProviderHeaders(
|
|
51
|
+
models: ProviderModelConfig[],
|
|
52
|
+
): Record<string, string> {
|
|
53
|
+
const modelHeaders = models.find((m) => m.headers)?.headers ?? {};
|
|
54
|
+
return {
|
|
55
|
+
...APERTURE_PROVENANCE_HEADERS,
|
|
56
|
+
...modelHeaders,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
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
|
+
/**
|
|
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
|
|
81
|
+
*/
|
|
82
|
+
export async function applyAperture(
|
|
83
|
+
pi: ExtensionAPI,
|
|
84
|
+
registry: ExtensionContext["modelRegistry"],
|
|
85
|
+
): Promise<string[]> {
|
|
86
|
+
const baseUrl = resolveApertureProviderBaseUrl();
|
|
87
|
+
const gatewayUrl = resolveGatewayUrl();
|
|
88
|
+
if (!baseUrl || !gatewayUrl) return [];
|
|
89
|
+
|
|
90
|
+
const { providers } = configLoader.getConfig();
|
|
91
|
+
|
|
92
|
+
let modelCache: Map<string, string[]>;
|
|
93
|
+
try {
|
|
94
|
+
modelCache = await getOrLoadProviderModelsCache(gatewayUrl, providers);
|
|
95
|
+
} catch {
|
|
96
|
+
modelCache = new Map();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
for (const provider of providers) {
|
|
100
|
+
const existingModels = registry
|
|
101
|
+
.getAll()
|
|
102
|
+
.filter((m) => m.provider === provider) as ProviderModelConfig[];
|
|
103
|
+
|
|
104
|
+
const models = mergeModels(existingModels, modelCache.get(provider));
|
|
105
|
+
|
|
106
|
+
pi.registerProvider(provider, {
|
|
107
|
+
baseUrl,
|
|
108
|
+
apiKey: "-",
|
|
109
|
+
headers: resolveProviderHeaders(models),
|
|
110
|
+
...(models.length > 0 && { api: models[0].api, models }),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
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
|
+
const gatewayUrl = resolveGatewayUrl();
|
|
126
|
+
if (!baseUrl || !gatewayUrl) return;
|
|
127
|
+
|
|
128
|
+
const { providers } = configLoader.getConfig();
|
|
129
|
+
|
|
130
|
+
let modelCache: Map<string, string[]>;
|
|
131
|
+
try {
|
|
132
|
+
modelCache = await fetchApertureProviderModels(gatewayUrl, providers);
|
|
133
|
+
setProviderModelsCache(modelCache);
|
|
134
|
+
} catch {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
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
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Re-resolve and set current model after provider registry updates. */
|
|
157
|
+
export async function refreshActiveModel(
|
|
158
|
+
pi: ExtensionAPI,
|
|
159
|
+
ctx: ExtensionContext,
|
|
160
|
+
): Promise<boolean> {
|
|
161
|
+
if (!ctx.model) return false;
|
|
162
|
+
|
|
163
|
+
const updated = ctx.modelRegistry.find(ctx.model.provider, ctx.model.id);
|
|
164
|
+
if (!updated) return false;
|
|
165
|
+
|
|
166
|
+
return pi.setModel(updated);
|
|
167
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
}
|