pi-wafer-provider 1.0.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.
@@ -0,0 +1,4 @@
1
+ github: monotykamary
2
+ ko_fi: monotykamary
3
+ buy_me_a_coffee: monotykamary
4
+ polar: monotykamary
@@ -0,0 +1 @@
1
+ 019da9d5-6cb9-764d-a39e-ce1dbd18a3b2
@@ -0,0 +1 @@
1
+ {"_meta":true,"v":1,"id":"memory","type":"named","createdAt":"2026-05-22T02:22:08.562Z","description":"Cross-session knowledge and insights"}
@@ -0,0 +1 @@
1
+ 019e9c1b-cad3-7bd6-a910-c015d0788505
package/AGENTS.md ADDED
@@ -0,0 +1,56 @@
1
+ # AGENTS.md
2
+
3
+ ## DO NOT EDIT — Auto-generated Files
4
+
5
+ The following files are **idempotent** and regenerated by `scripts/update-models.js`. Never edit them directly — your changes will be overwritten on the next model sync.
6
+
7
+ | File | Why it's auto-generated |
8
+ |------|------------------------|
9
+ | `models.json` | Built from the provider API. `update-models.js` fetches models, preserves curated data for known IDs, and writes this file. |
10
+ | `README.md` (model table) | The table under `## Available Models` is replaced in-place by `update-models.js` after merging base models → patch → custom models. |
11
+
12
+ ## Correct Files to Edit
13
+
14
+ When a model needs overrides, new properties, or corrections, edit the appropriate source file below. These are the **source of truth** that the update script reads but never writes.
15
+
16
+ | File | Purpose |
17
+ |------|---------|
18
+ | `patch.json` | Per-model overrides keyed by model ID. Add reasoning flags, compat settings, pricing corrections, thinking level maps, etc. Applied on top of `models.json` at runtime and for README generation. |
19
+ | `custom-models.json` | Models that don't exist in the provider API (hidden models, router endpoints, cross-provider aliases). Merged after patch. Format: array of full model objects (same schema as `models.json` entries). |
20
+ | `index.ts` | Provider extension code. |
21
+ | `scripts/update-models.js` | The sync script itself (edit only if changing how models are fetched/transformed). |
22
+
23
+ ## Data Flow
24
+
25
+ ```
26
+ Provider API ──fetch──► models.json ──apply──► patch.json ──merge──► custom-models.json
27
+ │ │ │
28
+ └────────────────────────────┴──────────────────────┘
29
+
30
+ README model table
31
+ ```
32
+
33
+ 1. `models.json` — base data from the provider API (auto-generated, DO NOT EDIT)
34
+ 2. `patch.json` — overrides applied on top (EDIT THIS for corrections/enrichments)
35
+ 3. `custom-models.json` — additional models not in the API (EDIT THIS for new models)
36
+ 4. README table — rendered from the merged result of all three (auto-generated, DO NOT EDIT)
37
+
38
+ ## Common Tasks
39
+
40
+ ### Add a compat setting or override pricing for an existing model
41
+ → Edit `patch.json`. Add an entry keyed by the model's `id`.
42
+
43
+ ### Add a model not available in the provider API
44
+ → Edit `custom-models.json`. Add a full model object to the array.
45
+
46
+ ### Update models from the provider API
47
+ → Run `node scripts/update-models.js` (may require an API key env var).
48
+
49
+ ### Regenerate the README model table
50
+ → Run `node scripts/update-models.js` — it updates both `models.json` and the README table.
51
+
52
+ ## TL;DR
53
+
54
+ - **Never edit `models.json`** — edit `patch.json` instead.
55
+ - **Never edit the README model table** — run the update script instead.
56
+ - `patch.json` and `custom-models.json` are the source files you should modify.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,137 @@
1
+ <div align="center">
2
+
3
+ # 🧇 pi-wafer-provider
4
+
5
+ **Qwen3.5, GLM-5.1, Kimi K2.6 & more via [Wafer](https://wafer.ai)**
6
+
7
+ _Serverless inference with free DeepSeek V4 models for [pi](https://github.com/earendil-works/pi-coding-agent)._
8
+
9
+ [![pi extension](https://img.shields.io/badge/pi-extension-blueviolet)](https://github.com/earendil-works/pi-coding-agent)
10
+ [![license](https://img.shields.io/badge/license-MIT-blue)](./LICENSE)
11
+
12
+ </div>
13
+
14
+ ---
15
+
16
+ ## Features
17
+
18
+ - **Fast Open-Source Models** via Wafer Serverless
19
+ - **Unified API** via Wafer's OpenAI-compatible completions endpoint
20
+ - **Cost Tracking** with per-model pricing for budget management
21
+ - **Reasoning Models** support for advanced reasoning capabilities
22
+ - **Vision Support** for Qwen3.5 (image + text input)
23
+
24
+ ## Provider
25
+
26
+ | Provider | API Key Env Var | Auth JSON Key |
27
+ |----------|----------------|---------------|
28
+ | `wafer-serverless` | `WAFER_SERVERLESS_API_KEY` | `wafer-serverless` |
29
+
30
+ Use `/model` in pi to select from available models.
31
+
32
+ ## Installation
33
+
34
+ ### Option 1: Using `pi install` (Recommended)
35
+
36
+ Install directly from GitHub:
37
+
38
+ ```bash
39
+ pi install https://github.com/monotykamary/pi-wafer-provider
40
+ ```
41
+
42
+ Then set your API key and run pi:
43
+ ```bash
44
+ # Recommended: add to auth.json
45
+ # See Authentication section below
46
+
47
+ # Or set as environment variables
48
+ export WAFER_SERVERLESS_API_KEY=your-serverless-key
49
+
50
+ pi
51
+ ```
52
+
53
+ ### Option 2: Manual Clone
54
+
55
+ 1. Clone this repository:
56
+ ```bash
57
+ git clone https://github.com/monotykamary/pi-wafer-provider.git
58
+ cd pi-wafer-provider
59
+ ```
60
+
61
+ 2. Set your Wafer API key:
62
+ ```bash
63
+ # Recommended: add to auth.json
64
+ # See Authentication section below
65
+
66
+ # Or set as environment variables
67
+ export WAFER_SERVERLESS_API_KEY=your-serverless-key
68
+ ```
69
+
70
+ 3. Run pi with the extension:
71
+ ```bash
72
+ pi -e /path/to/pi-wafer-provider
73
+ ```
74
+
75
+ ## Available Models
76
+
77
+ | Model | Type | Context | Max Output | Input Cost | Output Cost | Cached Input |
78
+ |-------|------|---------|------------|------------|-------------|--------------|
79
+ | Deepseek V4 Flash | Text | 1M | 16K | Free | Free | Free |
80
+ | Deepseek V4 Pro | Text | 1M | 384K | Free | Free | Free |
81
+ | GLM 5.1 | Text | 203K | 33K | $1.50 | $4.50 | $0.15 |
82
+ | GLM 5.2 | Text | 1M | 16K | Free | Free | Free |
83
+ | Kimi K2.6 | Text | 262K | 33K | $1.10 | $4.80 | $0.11 |
84
+ | Kimi K2.7 Code | Text | 262K | 16K | Free | Free | Free |
85
+ | MiniMax M3 | Text | 1M | 16K | Free | Free | Free |
86
+ | Qwen 3.5 397B (A17B) | Text + Image | 262K | 33K | $0.60 | $3.60 | $0.06 |
87
+ | Qwen3.6 35B A3B | Text | 256K | 16K | Free | Free | Free |
88
+ | Qwen3.7 Max | Text | 256K | 16K | $5.00 | $15.00 | $0.50 |
89
+
90
+ *Costs are per million tokens. Prices based on official provider pricing.*
91
+
92
+ ## Usage
93
+
94
+ After loading the extension, use the `/model` command in pi to select your preferred model:
95
+
96
+ ```
97
+ /model
98
+ ```
99
+
100
+ Then select `wafer-serverless` as the provider and choose from the available models.
101
+
102
+ ## Authentication
103
+
104
+ API keys can be configured in multiple ways (resolved in this order):
105
+
106
+ 1. **`auth.json`** (recommended) — Add to `~/.pi/agent/auth.json`:
107
+ ```json
108
+ {
109
+ "wafer-serverless": { "type": "api_key", "key": "your-serverless-key" }
110
+ }
111
+ ```
112
+ The `key` field supports literal values, env var names, and shell commands (prefix with `!`). See [pi's auth file docs](https://github.com/badlogic/pi-mono) for details.
113
+ 2. **Runtime override** — Use the `--api-key` CLI flag
114
+ 3. **Environment variable** — Set `WAFER_SERVERLESS_API_KEY` (falls back to `WAFER_API_KEY` for backwards compatibility)
115
+
116
+ ## Environment Variables
117
+
118
+ | Variable | Provider | Description |
119
+ |----------|----------|-------------|
120
+ | `WAFER_SERVERLESS_API_KEY` | `wafer-serverless` | Primary API key |
121
+ | `WAFER_API_KEY` | `wafer-serverless` | Legacy fallback (used if `WAFER_SERVERLESS_API_KEY` is unset) |
122
+
123
+ ## Configuration
124
+
125
+ Add to your pi configuration for automatic loading:
126
+
127
+ ```json
128
+ {
129
+ "extensions": [
130
+ "/path/to/pi-wafer-provider"
131
+ ]
132
+ }
133
+ ```
134
+
135
+ ## License
136
+
137
+ MIT
@@ -0,0 +1 @@
1
+ []
package/index.ts ADDED
@@ -0,0 +1,400 @@
1
+ /**
2
+ * Wafer Provider Extension
3
+ *
4
+ * Registers Wafer Serverless as a custom provider using the
5
+ * OpenAI completions API.
6
+ *
7
+ * Model resolution strategy: Stale-While-Revalidate
8
+ * 1. Serve stale immediately: disk cache → embedded models.json (zero-latency)
9
+ * 2. Revalidate in background: live API /models → merge with embedded → cache → hot-swap
10
+ * 3. patch.json + custom-models.json applied on top of whichever source won
11
+ *
12
+ * Merge order: [live|cache|embedded] → apply patch.json → merge custom-models.json
13
+ *
14
+ * Provider:
15
+ * - wafer-serverless (WAFER_SERVERLESS_API_KEY, falls back to WAFER_API_KEY)
16
+ *
17
+ * Usage:
18
+ * # Option 1: Store in auth.json (recommended)
19
+ * # Add to ~/.pi/agent/auth.json:
20
+ * # "wafer-serverless": { "type": "api_key", "key": "your-wafer-serverless-key" }
21
+ *
22
+ * # Option 2: Set as environment variables
23
+ * export WAFER_SERVERLESS_API_KEY=your-wafer-serverless-key
24
+ * # Falls back to WAFER_API_KEY for backwards compatibility
25
+ *
26
+ * # Run pi with the extension
27
+ * pi -e /path/to/pi-wafer-provider
28
+ *
29
+ * Then use /model to select available models:
30
+ * - Qwen3.5-397B-A17B (262K context)
31
+ * - GLM-5.1 (202K context)
32
+ */
33
+
34
+ import { getAgentDir, type ExtensionAPI, type ModelRegistry } from "@earendil-works/pi-coding-agent";
35
+ import modelsData from "./models.json" with { type: "json" };
36
+ import customModelsData from "./custom-models.json" with { type: "json" };
37
+ import patchData from "./patch.json" with { type: "json" };
38
+ import fs from "fs";
39
+ import path from "path";
40
+
41
+ // ─── Types ────────────────────────────────────────────────────────────────────
42
+
43
+ interface JsonModel {
44
+ id: string;
45
+ name: string;
46
+ reasoning: boolean;
47
+ input: string[];
48
+ cost: {
49
+ input: number;
50
+ output: number;
51
+ cacheRead: number;
52
+ cacheWrite: number;
53
+ };
54
+ contextWindow: number;
55
+ maxTokens: number;
56
+ thinkingLevelMap?: Record<string, string | null>;
57
+ providers?: string[];
58
+ headers?: Record<string, string>;
59
+ compat?: {
60
+ supportsDeveloperRole?: boolean;
61
+ supportsStore?: boolean;
62
+ maxTokensField?: "max_completion_tokens" | "max_tokens";
63
+ thinkingFormat?: "openai" | "zai" | "qwen" | "qwen-chat-template";
64
+ supportsReasoningEffort?: boolean;
65
+ supportsZdr?: boolean;
66
+ };
67
+ }
68
+
69
+ interface PatchEntry {
70
+ name?: string;
71
+ reasoning?: boolean;
72
+ input?: string[];
73
+ cost?: {
74
+ input?: number;
75
+ output?: number;
76
+ cacheRead?: number;
77
+ cacheWrite?: number;
78
+ };
79
+ contextWindow?: number;
80
+ maxTokens?: number;
81
+ thinkingLevelMap?: Record<string, string | null>;
82
+ providers?: string[];
83
+ compat?: Record<string, unknown>;
84
+ }
85
+
86
+ type PatchData = Record<string, PatchEntry>;
87
+
88
+ // ─── Patch Application ─────────────────────────────────────────────────────────
89
+
90
+ function applyPatch(model: JsonModel, patch: PatchEntry): JsonModel {
91
+ const result = { ...model };
92
+
93
+ if (patch.name !== undefined) result.name = patch.name;
94
+ if (patch.reasoning !== undefined) result.reasoning = patch.reasoning;
95
+ if (patch.input !== undefined) result.input = patch.input;
96
+ if (patch.contextWindow !== undefined) result.contextWindow = patch.contextWindow;
97
+ if (patch.maxTokens !== undefined) result.maxTokens = patch.maxTokens;
98
+ if (patch.providers !== undefined) result.providers = patch.providers;
99
+
100
+ if (patch.cost) {
101
+ result.cost = {
102
+ input: patch.cost.input ?? result.cost.input,
103
+ output: patch.cost.output ?? result.cost.output,
104
+ cacheRead: patch.cost.cacheRead ?? result.cost.cacheRead,
105
+ cacheWrite: patch.cost.cacheWrite ?? result.cost.cacheWrite,
106
+ };
107
+ }
108
+ if (patch.compat) {
109
+ result.compat = { ...(result.compat || {}), ...patch.compat };
110
+ }
111
+
112
+ if (!result.reasoning && result.compat?.thinkingFormat) {
113
+ delete result.compat.thinkingFormat;
114
+ }
115
+ if (result.compat && Object.keys(result.compat).length === 0) {
116
+ delete result.compat;
117
+ }
118
+
119
+ return result;
120
+ }
121
+
122
+ /** Full pipeline: base models → patch → custom → result */
123
+ function buildModels(base: JsonModel[], custom: JsonModel[], patch: PatchData): JsonModel[] {
124
+ const modelMap = new Map<string, JsonModel>();
125
+
126
+ for (const model of base) {
127
+ modelMap.set(model.id, model);
128
+ }
129
+
130
+ for (const [id, patchEntry] of Object.entries(patch)) {
131
+ const existing = modelMap.get(id);
132
+ if (existing) {
133
+ modelMap.set(id, applyPatch(existing, patchEntry));
134
+ }
135
+ }
136
+
137
+ for (const model of custom) {
138
+ const existing = modelMap.get(model.id);
139
+ const patchEntry = patch[model.id];
140
+ if (existing && patchEntry) {
141
+ modelMap.set(model.id, applyPatch(model, patchEntry));
142
+ } else if (existing) {
143
+ modelMap.set(model.id, model);
144
+ } else if (patchEntry) {
145
+ modelMap.set(model.id, applyPatch(model, patchEntry));
146
+ } else {
147
+ modelMap.set(model.id, model);
148
+ }
149
+ }
150
+
151
+ return Array.from(modelMap.values());
152
+ }
153
+
154
+ /** Filter models to only those belonging to the given provider, then strip the `providers` field. */
155
+ function filterModelsForProvider(models: JsonModel[], providerId: string): JsonModel[] {
156
+ return models
157
+ .filter((m) => !m.providers || m.providers.includes(providerId))
158
+ .map(({ providers, ...rest }) => rest);
159
+ }
160
+
161
+ /** Apply per-model ZDR header unless the model explicitly opts out. */
162
+ function applyZdrHeaders(models: JsonModel[]): JsonModel[] {
163
+ return models.map((model) => {
164
+ // default: ZDR supported; omit header only if compat.supportsZdr === false
165
+ if (model.compat?.supportsZdr === false) return model;
166
+ return {
167
+ ...model,
168
+ headers: { ...(model.headers || {}), "Wafer-ZDR": "required" },
169
+ };
170
+ });
171
+ }
172
+
173
+ // ─── Stale-While-Revalidate Model Sync ────────────────────────────────────────
174
+
175
+ const BASE_URL = "https://pass.wafer.ai/v1";
176
+ const MODELS_URL = `${BASE_URL}/models`;
177
+ const CACHE_DIR = path.join(getAgentDir(), "cache");
178
+ const LIVE_FETCH_TIMEOUT_MS = 8000;
179
+
180
+ interface ProviderConfig {
181
+ providerId: string;
182
+ apiKeyEnv: string;
183
+ fallbackApiKeyEnv?: string;
184
+ }
185
+
186
+ /** Transform a model from the Wafer /v1/models API. */
187
+ function transformApiModel(apiModel: any): JsonModel | null {
188
+ return {
189
+ id: apiModel.id,
190
+ name: apiModel.id,
191
+ reasoning: false,
192
+ input: ["text"],
193
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
194
+ contextWindow: apiModel.max_model_len || 0,
195
+ maxTokens: 0,
196
+ compat: {
197
+ supportsZdr: apiModel.zdr_supported ?? undefined,
198
+ supportsReasoningEffort: true,
199
+ },
200
+ };
201
+ }
202
+
203
+ function getCachePath(providerId: string): string {
204
+ return path.join(CACHE_DIR, `${providerId}-models.json`);
205
+ }
206
+
207
+ async function fetchLiveModels(apiKey: string, signal?: AbortSignal): Promise<JsonModel[] | null> {
208
+ try {
209
+ const response = await fetch(MODELS_URL, {
210
+ headers: { Authorization: `Bearer ${apiKey}` },
211
+ signal: signal ? AbortSignal.any([AbortSignal.timeout(LIVE_FETCH_TIMEOUT_MS), signal]) : AbortSignal.timeout(LIVE_FETCH_TIMEOUT_MS),
212
+ });
213
+ if (!response.ok) return null;
214
+ const data = await response.json();
215
+ const apiModels = Array.isArray(data) ? data : (data.data || []);
216
+ if (!Array.isArray(apiModels) || apiModels.length === 0) return null;
217
+ return apiModels.map(transformApiModel).filter((m): m is JsonModel => m !== null);
218
+ } catch {
219
+ return null;
220
+ }
221
+ }
222
+
223
+ function loadCachedModels(providerId: string): JsonModel[] | null {
224
+ try {
225
+ const data = JSON.parse(fs.readFileSync(getCachePath(providerId), "utf8"));
226
+ return Array.isArray(data) ? data : null;
227
+ } catch {
228
+ return null;
229
+ }
230
+ }
231
+
232
+ function cacheModels(providerId: string, models: JsonModel[]): void {
233
+ try {
234
+ fs.mkdirSync(CACHE_DIR, { recursive: true });
235
+ fs.writeFileSync(getCachePath(providerId), JSON.stringify(models, null, 2) + "\n");
236
+ } catch {
237
+ // Cache write failure is non-fatal
238
+ }
239
+ }
240
+
241
+ function mergeWithEmbedded(liveModels: JsonModel[], embeddedModels: JsonModel[]): JsonModel[] {
242
+ const embeddedMap = new Map(embeddedModels.map(m => [m.id, m]));
243
+ const seen = new Set<string>();
244
+ const result: JsonModel[] = [];
245
+ for (const liveModel of liveModels) {
246
+ const embedded = embeddedMap.get(liveModel.id);
247
+ seen.add(liveModel.id);
248
+ if (embedded) {
249
+ // Self-heal: live API pricing is authoritative field-by-field. Prefer the
250
+ // live cost when the API reports it (non-zero); fall back to embedded when
251
+ // the API is silent (0) so curated cacheRead/cacheWrite isn't clobbered and
252
+ // providers whose /models endpoint exposes no pricing keep their curated
253
+ // cost. Curation (reasoning/input/compat/name) still wins via ...embedded.
254
+ result.push({
255
+ ...liveModel,
256
+ ...embedded,
257
+ cost: {
258
+ input: liveModel.cost.input || embedded.cost.input,
259
+ output: liveModel.cost.output || embedded.cost.output,
260
+ cacheRead: liveModel.cost.cacheRead || embedded.cost.cacheRead,
261
+ cacheWrite: liveModel.cost.cacheWrite || embedded.cost.cacheWrite,
262
+ },
263
+ contextWindow: liveModel.contextWindow || embedded.contextWindow,
264
+ });
265
+ } else {
266
+ result.push(liveModel);
267
+ }
268
+ }
269
+ // Append any embedded models that the live API didn't return
270
+ for (const em of embeddedModels) {
271
+ if (!seen.has(em.id)) {
272
+ result.push(em);
273
+ }
274
+ }
275
+ return result;
276
+ }
277
+
278
+ function loadStaleModels(providerId: string, embeddedModels: JsonModel[]): JsonModel[] {
279
+ const cached = loadCachedModels(providerId);
280
+ if (!cached || cached.length === 0) return embeddedModels;
281
+
282
+ // Merge embedded models that are missing from cache (newly added models)
283
+ const cachedMap = new Map(cached.map(m => [m.id, m]));
284
+ for (const em of embeddedModels) {
285
+ if (!cachedMap.has(em.id)) {
286
+ cached.push(em);
287
+ }
288
+ }
289
+ return cached;
290
+ }
291
+
292
+ async function revalidateModels(providerId: string, apiKey: string | undefined, embeddedModels: JsonModel[], signal?: AbortSignal): Promise<JsonModel[] | null> {
293
+ if (!apiKey) return null;
294
+ const liveModels = await fetchLiveModels(apiKey, signal);
295
+ if (!liveModels || liveModels.length === 0) return null;
296
+ const merged = mergeWithEmbedded(liveModels, embeddedModels);
297
+ cacheModels(providerId, merged);
298
+ return merged;
299
+ }
300
+
301
+ // ─── Per-Provider State ───────────────────────────────────────────────────────
302
+
303
+ interface ProviderState {
304
+ cachedApiKey: string | undefined;
305
+ revalidateAbort: AbortController | null;
306
+ }
307
+
308
+ function createProviderState(): ProviderState {
309
+ return { cachedApiKey: undefined, revalidateAbort: null };
310
+ }
311
+
312
+ /** Resolve API key from pi auth registry, falling back to env vars. */
313
+ async function resolveApiKey(state: ProviderState, providerId: string, modelRegistry: ModelRegistry, config: ProviderConfig): Promise<void> {
314
+ state.cachedApiKey =
315
+ (await modelRegistry.getApiKeyForProvider(providerId))
316
+ ?? resolveEnvKeyValue(config)
317
+ ?? undefined;
318
+ }
319
+
320
+ /** Resolve the actual API key value from environment (for fetch calls). */
321
+ function resolveEnvKeyValue(config: ProviderConfig): string | undefined {
322
+ const { apiKeyEnv, fallbackApiKeyEnv } = config;
323
+ return process.env[apiKeyEnv] || (fallbackApiKeyEnv ? process.env[fallbackApiKeyEnv] : undefined);
324
+ }
325
+
326
+ /** Resolve the $-prefixed env var reference for registerProvider.
327
+ * Uses the primary key if set, otherwise the fallback key if set,
328
+ * otherwise defaults to the primary key name (pi marks it unresolved). */
329
+ function resolveEnvKeyRef(config: ProviderConfig): string {
330
+ const { apiKeyEnv, fallbackApiKeyEnv } = config;
331
+ if (process.env[apiKeyEnv]) return `$${apiKeyEnv}`;
332
+ if (fallbackApiKeyEnv && process.env[fallbackApiKeyEnv]) return `$${fallbackApiKeyEnv}`;
333
+ return `$${apiKeyEnv}`;
334
+ }
335
+
336
+ function registerWaferProvider(
337
+ pi: ExtensionAPI,
338
+ config: ProviderConfig,
339
+ embeddedModels: JsonModel[],
340
+ customModels: JsonModel[],
341
+ patches: PatchData,
342
+ ): void {
343
+ const { providerId } = config;
344
+ const state = createProviderState();
345
+
346
+ const staleBase = loadStaleModels(providerId, embeddedModels);
347
+ const staleModels = filterModelsForProvider(
348
+ buildModels(staleBase, customModels, patches),
349
+ providerId,
350
+ );
351
+
352
+ pi.registerProvider(providerId, {
353
+ baseUrl: BASE_URL,
354
+ apiKey: resolveEnvKeyRef(config),
355
+ api: "openai-completions",
356
+ models: applyZdrHeaders(staleModels),
357
+ });
358
+
359
+ pi.on("session_start", async (_event, ctx) => {
360
+ state.revalidateAbort?.abort();
361
+ state.revalidateAbort = new AbortController();
362
+ const signal = state.revalidateAbort.signal;
363
+ resolveApiKey(state, providerId, ctx.modelRegistry, config).then(() => {
364
+ revalidateModels(providerId, state.cachedApiKey, embeddedModels, signal).then((freshBase) => {
365
+ if (freshBase && !signal.aborted) {
366
+ pi.registerProvider(providerId, {
367
+ baseUrl: BASE_URL,
368
+ apiKey: resolveEnvKeyRef(config),
369
+ api: "openai-completions",
370
+ models: applyZdrHeaders(
371
+ filterModelsForProvider(buildModels(freshBase, customModels, patches), providerId),
372
+ ),
373
+ });
374
+ }
375
+ });
376
+ });
377
+ });
378
+
379
+ pi.on("session_shutdown", () => {
380
+ state.revalidateAbort?.abort();
381
+ });
382
+ }
383
+
384
+ // ─── Provider Definitions ────────────────────────────────────────────────────
385
+
386
+ const PROVIDER: ProviderConfig = {
387
+ providerId: "wafer-serverless",
388
+ apiKeyEnv: "WAFER_SERVERLESS_API_KEY",
389
+ fallbackApiKeyEnv: "WAFER_API_KEY",
390
+ };
391
+
392
+ // ─── Extension Entry Point ────────────────────────────────────────────────────
393
+
394
+ export default function (pi: ExtensionAPI) {
395
+ const embeddedModels = modelsData as JsonModel[];
396
+ const customModels = customModelsData as JsonModel[];
397
+ const patches = patchData as PatchData;
398
+
399
+ registerWaferProvider(pi, PROVIDER, embeddedModels, customModels, patches);
400
+ }