@billjr99/pi-openai-compat 1.1.12 → 1.1.14
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 +22 -1
- package/index.ts +164 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -77,12 +77,21 @@ If pi is already running when you install, type `/reload` first.
|
|
|
77
77
|
> | **Cloudflare Workers AI** | HTTP 405 (no `GET /v1/models`; real catalog at `/ai/models/search`, id field is `name`, mixed task types) | Live discovery — `modelsUrl: …/ai/models/search`, `modelsIdField: name`, `modelsKeepTask: "Text Generation"` |
|
|
78
78
|
> | **Cloudflare AI Gateway** | HTTP 401 (token missing `AI Gateway: Run`) or HTTP 400 *"Please configure AI Gateway"* (gateway slug doesn't exist, or the upstream isn't configured on it) | Built-in fallback list |
|
|
79
79
|
> | **Hugging Face** | Returns HTML rather than JSON | Built-in fallback list |
|
|
80
|
+
>
|
|
81
|
+
> **Auto-heal for older configs:** if you logged in before live discovery
|
|
82
|
+
> existed, your saved provider is missing these `modelsUrl`/`modelsIdField`/
|
|
83
|
+
> `modelsKeepTask` fields. On startup the extension backfills them from the
|
|
84
|
+
> template whenever it can do so without prompting — i.e. when your saved base
|
|
85
|
+
> URL still matches the template (Cloudflare Workers AI). When the base URL
|
|
86
|
+
> itself changed (e.g. GitHub Models' retired `models.inference.ai.azure.com`
|
|
87
|
+
> host), it can't be migrated silently and you'll get a one-time notice to run
|
|
88
|
+
> `/compat-login`.
|
|
80
89
|
|
|
81
90
|
---
|
|
82
91
|
|
|
83
92
|
## Commands
|
|
84
93
|
|
|
85
|
-
|
|
94
|
+
Three commands are available; `/compat-login` is the only one you need to get started.
|
|
86
95
|
|
|
87
96
|
### `/compat-login`
|
|
88
97
|
|
|
@@ -98,6 +107,18 @@ After login, the provider's models appear in pi's `/model` command and
|
|
|
98
107
|
`Ctrl+L` picker immediately. You can run `/compat-login` again to add a
|
|
99
108
|
second provider — all providers are active simultaneously.
|
|
100
109
|
|
|
110
|
+
### `/compat-refresh`
|
|
111
|
+
|
|
112
|
+
Re-fetches the model list for an already-registered provider, reusing the
|
|
113
|
+
saved base URL, API key, and discovery settings — no need to re-enter URLs,
|
|
114
|
+
keys, or account IDs. Use it to pick up models a provider has added (or
|
|
115
|
+
dropped) since you logged in, without restarting your session.
|
|
116
|
+
|
|
117
|
+
If you have multiple providers registered, you are asked which one to refresh
|
|
118
|
+
(or choose **All providers**); with a single provider it refreshes directly.
|
|
119
|
+
A failed or empty refresh leaves the existing model list untouched, so a flaky
|
|
120
|
+
network call can't blank out a working provider.
|
|
121
|
+
|
|
101
122
|
### `/compat-logout`
|
|
102
123
|
|
|
103
124
|
Unregisters a provider from pi. If you have multiple providers registered,
|
package/index.ts
CHANGED
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
* - Every provider saved in config.json is registered automatically at
|
|
9
9
|
* startup. The factory is async so pi waits for registration to complete
|
|
10
10
|
* before showing the model list — no session_start delay.
|
|
11
|
-
* - /compat-login
|
|
12
|
-
* - /compat-
|
|
11
|
+
* - /compat-login adds a provider (fetches fresh model list, registers).
|
|
12
|
+
* - /compat-refresh re-fetches the model list for a registered provider.
|
|
13
|
+
* - /compat-logout removes a provider (unregisters, restores previous model).
|
|
13
14
|
* - No activeProviders list — presence in config.providers means registered.
|
|
14
15
|
*
|
|
15
16
|
* Config: ~/.config/pi-openai-compat/config.json
|
|
@@ -348,6 +349,84 @@ function saveConfig(config: ExtensionConfig): void {
|
|
|
348
349
|
}
|
|
349
350
|
}
|
|
350
351
|
|
|
352
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
353
|
+
// Discovery-field auto-heal
|
|
354
|
+
//
|
|
355
|
+
// Providers saved before model-discovery overrides existed (modelsUrl/
|
|
356
|
+
// modelsIdField/modelsKeepTask) have none on their config, so /compat-refresh
|
|
357
|
+
// and the session_start rehydrate hit the broken default <baseUrl>/models path
|
|
358
|
+
// (e.g. Cloudflare Workers AI's 405). When a provider's saved baseUrl still
|
|
359
|
+
// matches its template — so we can recover any account-id/slug placeholders —
|
|
360
|
+
// we backfill the discovery fields from the template. Providers whose baseUrl
|
|
361
|
+
// no longer matches the template (e.g. GitHub Models' retired Azure host) can't
|
|
362
|
+
// be healed safely and are reported so the user can re-run /compat-login.
|
|
363
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
364
|
+
|
|
365
|
+
/** Placeholders that may appear in a template's baseUrl / modelsUrl. */
|
|
366
|
+
const URL_PLACEHOLDERS = ["YOUR_ACCOUNT_ID", "YOUR_GATEWAY_SLUG", "YOUR_PROVIDER"];
|
|
367
|
+
|
|
368
|
+
function escapeRegex(s: string): string {
|
|
369
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Recover placeholder values by matching a saved URL against a template URL.
|
|
374
|
+
* Returns a map (possibly empty when the template has no placeholders) when the
|
|
375
|
+
* saved URL is consistent with the template, or null when it isn't — which we
|
|
376
|
+
* treat as "this provider can't be healed automatically".
|
|
377
|
+
*/
|
|
378
|
+
function recoverPlaceholders(templateUrl: string, savedUrl: string): Record<string, string> | null {
|
|
379
|
+
const order: string[] = [];
|
|
380
|
+
const placeholderRe = new RegExp(URL_PLACEHOLDERS.join("|"), "g");
|
|
381
|
+
let source = "^";
|
|
382
|
+
let lastIndex = 0;
|
|
383
|
+
let m: RegExpExecArray | null;
|
|
384
|
+
while ((m = placeholderRe.exec(templateUrl)) !== null) {
|
|
385
|
+
source += escapeRegex(templateUrl.slice(lastIndex, m.index)) + "([^/]+)";
|
|
386
|
+
order.push(m[0]);
|
|
387
|
+
lastIndex = m.index + m[0].length;
|
|
388
|
+
}
|
|
389
|
+
source += escapeRegex(templateUrl.slice(lastIndex)) + "$";
|
|
390
|
+
|
|
391
|
+
const match = new RegExp(source).exec(savedUrl);
|
|
392
|
+
if (!match) return null;
|
|
393
|
+
const out: Record<string, string> = {};
|
|
394
|
+
order.forEach((name, i) => { out[name] = match[i + 1]; });
|
|
395
|
+
return out;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function applyPlaceholders(url: string, values: Record<string, string>): string {
|
|
399
|
+
let out = url;
|
|
400
|
+
for (const [name, value] of Object.entries(values)) out = out.split(name).join(value);
|
|
401
|
+
return out;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Backfill missing discovery fields on saved providers from their template.
|
|
406
|
+
* Mutates `config` in place; the caller is responsible for persisting when
|
|
407
|
+
* `healed` is non-empty. `stale` lists providers that need a manual re-login.
|
|
408
|
+
*/
|
|
409
|
+
function migrateDiscoveryFields(config: ExtensionConfig): { healed: string[]; stale: string[] } {
|
|
410
|
+
const healed: string[] = [];
|
|
411
|
+
const stale: string[] = [];
|
|
412
|
+
|
|
413
|
+
for (const [key, p] of Object.entries(config.providers)) {
|
|
414
|
+
const tpl = TEMPLATES[key];
|
|
415
|
+
if (!tpl?.modelsUrl) continue; // no matching template, or template needs no overrides
|
|
416
|
+
if (p.modelsUrl) continue; // already set (fresh login or manual edit) — never clobber
|
|
417
|
+
|
|
418
|
+
const values = recoverPlaceholders(tpl.baseUrl, p.baseUrl);
|
|
419
|
+
if (!values) { stale.push(p.displayName); continue; }
|
|
420
|
+
|
|
421
|
+
p.modelsUrl = applyPlaceholders(tpl.modelsUrl, values);
|
|
422
|
+
p.modelsIdField = tpl.modelsIdField;
|
|
423
|
+
p.modelsKeepTask = tpl.modelsKeepTask;
|
|
424
|
+
healed.push(p.displayName);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return { healed, stale };
|
|
428
|
+
}
|
|
429
|
+
|
|
351
430
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
352
431
|
// Networking
|
|
353
432
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -471,6 +550,11 @@ function registerProvider(pi: ExtensionAPI, key: string, p: ProviderConfig): voi
|
|
|
471
550
|
export default async function (pi: ExtensionAPI) {
|
|
472
551
|
let config = loadConfig();
|
|
473
552
|
|
|
553
|
+
// Self-heal older configs: backfill discovery fields (modelsUrl/idField/
|
|
554
|
+
// keepTask) we can derive from the template without prompting. Persist once
|
|
555
|
+
// so /compat-refresh and the session_start rehydrate use the fixed values.
|
|
556
|
+
if (migrateDiscoveryFields(config).healed.length > 0) saveConfig(config);
|
|
557
|
+
|
|
474
558
|
// Register all saved providers immediately using cached model lists.
|
|
475
559
|
// The factory is async, so pi waits for this to finish before startup
|
|
476
560
|
// continues — providers are visible in /model from the very first render.
|
|
@@ -486,6 +570,11 @@ export default async function (pi: ExtensionAPI) {
|
|
|
486
570
|
pi.on("session_start", async (_event, ctx) => {
|
|
487
571
|
config = loadConfig();
|
|
488
572
|
|
|
573
|
+
// Self-heal what we can (silently), and collect providers whose saved
|
|
574
|
+
// baseUrl no longer matches the template — those need a manual re-login.
|
|
575
|
+
const { healed, stale } = migrateDiscoveryFields(config);
|
|
576
|
+
if (healed.length > 0) saveConfig(config);
|
|
577
|
+
|
|
489
578
|
const registered: string[] = [];
|
|
490
579
|
const failed: string[] = [];
|
|
491
580
|
|
|
@@ -526,6 +615,13 @@ export default async function (pi: ExtensionAPI) {
|
|
|
526
615
|
"warning"
|
|
527
616
|
);
|
|
528
617
|
}
|
|
618
|
+
if (stale.length > 0) {
|
|
619
|
+
ctx.ui.notify(
|
|
620
|
+
`OpenAI-compat: ${stale.join(", ")} ${stale.length === 1 ? "has" : "have"} an out-of-date ` +
|
|
621
|
+
`base URL — run /compat-login to update (its endpoint changed and can't be migrated automatically).`,
|
|
622
|
+
"warning"
|
|
623
|
+
);
|
|
624
|
+
}
|
|
529
625
|
});
|
|
530
626
|
|
|
531
627
|
// ── model_select ───────────────────────────────────────────────────────────
|
|
@@ -690,6 +786,72 @@ export default async function (pi: ExtensionAPI) {
|
|
|
690
786
|
},
|
|
691
787
|
});
|
|
692
788
|
|
|
789
|
+
// ── /compat-refresh ──────────────────────────────────────────────────────────
|
|
790
|
+
// Force a re-fetch of an already-registered provider's model list, reusing
|
|
791
|
+
// the persisted baseUrl/apiKey and discovery overrides — no need to re-enter
|
|
792
|
+
// URLs, keys, or account IDs like /compat-login. Picks up newly added (or
|
|
793
|
+
// dropped) upstream models without restarting the session.
|
|
794
|
+
pi.registerCommand("compat-refresh", {
|
|
795
|
+
description: "Re-fetch the model list for a registered OpenAI-compatible provider",
|
|
796
|
+
handler: async (_args, ctx) => {
|
|
797
|
+
const providerKeys = Object.keys(config.providers);
|
|
798
|
+
if (!providerKeys.length) {
|
|
799
|
+
ctx.ui.notify("No compat providers are registered. Run /compat-login first.", "info");
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// Choose which provider(s) to refresh. With one provider, refresh it
|
|
804
|
+
// directly; otherwise offer each by name plus an "All providers" option.
|
|
805
|
+
let keys: string[];
|
|
806
|
+
if (providerKeys.length === 1) {
|
|
807
|
+
keys = providerKeys;
|
|
808
|
+
} else {
|
|
809
|
+
const ALL = "All providers";
|
|
810
|
+
const labels = [ALL, ...providerKeys.map((k) => config.providers[k].displayName)];
|
|
811
|
+
const chosen = await ctx.ui.select("Refresh which provider?", labels);
|
|
812
|
+
if (!chosen) { ctx.ui.notify("Cancelled.", "info"); return; }
|
|
813
|
+
keys = chosen === ALL ? providerKeys : [providerKeys[labels.indexOf(chosen) - 1]];
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
const refreshed: string[] = [];
|
|
817
|
+
const failed: string[] = [];
|
|
818
|
+
for (const key of keys) {
|
|
819
|
+
const p = config.providers[key];
|
|
820
|
+
ctx.ui.notify(`Refreshing ${p.displayName} …`, "info");
|
|
821
|
+
try {
|
|
822
|
+
const models = await fetchModels(p.baseUrl, p.apiKey, {
|
|
823
|
+
url: p.modelsUrl,
|
|
824
|
+
idField: p.modelsIdField,
|
|
825
|
+
keepTask: p.modelsKeepTask,
|
|
826
|
+
});
|
|
827
|
+
if (models.length > 0) {
|
|
828
|
+
// Only overwrite the cache on a successful, non-empty fetch — a
|
|
829
|
+
// flaky refresh must never blank out a working provider's models.
|
|
830
|
+
p.cachedModels = models;
|
|
831
|
+
saveConfig(config);
|
|
832
|
+
registerProvider(pi, key, p);
|
|
833
|
+
refreshed.push(`${p.displayName} (${models.length})`);
|
|
834
|
+
} else {
|
|
835
|
+
failed.push(p.displayName);
|
|
836
|
+
}
|
|
837
|
+
} catch {
|
|
838
|
+
failed.push(p.displayName);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
if (refreshed.length > 0) {
|
|
843
|
+
ctx.ui.notify(`Refreshed: ${refreshed.join(", ")}.`, "success");
|
|
844
|
+
}
|
|
845
|
+
if (failed.length > 0) {
|
|
846
|
+
ctx.ui.notify(
|
|
847
|
+
`Could not refresh ${failed.join(", ")} — kept the existing model list. ` +
|
|
848
|
+
`Run /compat-login if the provider's URL or key changed.`,
|
|
849
|
+
"warning"
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
},
|
|
853
|
+
});
|
|
854
|
+
|
|
693
855
|
// ── /compat-logout ─────────────────────────────────────────────────────────
|
|
694
856
|
pi.registerCommand("compat-logout", {
|
|
695
857
|
description: "Remove an OpenAI-compatible provider from pi's model list",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@billjr99/pi-openai-compat",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.14",
|
|
4
4
|
"description": "pi-coding-agent extension: OpenAI-compatible endpoint support (OpenRouter, NVIDIA NIM, Nous Portal, Ollama, custom)",
|
|
5
5
|
"author": "Bill Mongan <https://github.com/BillJr99>",
|
|
6
6
|
"license": "MIT",
|