@billjr99/pi-openai-compat 1.1.14 → 1.1.15

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.
Files changed (3) hide show
  1. package/README.md +10 -5
  2. package/index.ts +45 -13
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -311,11 +311,16 @@ parse error.
311
311
  **Cloudflare AI Gateway returns 401 Unauthorized**
312
312
  Check both: (1) the API token has `AI Gateway: Run` *and* `Workers AI: Read`
313
313
  under "Permissions", scoped to the correct account; and (2) the gateway slug
314
- in the URL actually exists under that account — list with
315
- `curl https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/ai-gateway/gateways
316
- -H "Authorization: Bearer $TOKEN"`. An empty `result` array means the gateway
317
- isn't there (you may need to create it in **dash.cloudflare.com → AI → AI
318
- Gateway**, or you're querying the wrong account).
314
+ in the URL actually exists under that account — list them with:
315
+
316
+ ```bash
317
+ curl https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/ai-gateway/gateways \
318
+ -H "Authorization: Bearer $TOKEN"
319
+ ```
320
+
321
+ An empty `result` array means the gateway isn't there (you may need to create it
322
+ in **dash.cloudflare.com → AI → AI Gateway**, or you're querying the wrong
323
+ account).
319
324
 
320
325
  **No models appear after login**
321
326
  For Ollama: pull at least one model first (`ollama pull llama3`).
package/index.ts CHANGED
@@ -44,6 +44,10 @@ interface ProviderConfig {
44
44
  modelsUrl?: string;
45
45
  modelsIdField?: string;
46
46
  modelsKeepTask?: string;
47
+ // Set once we've warned the user that this provider's saved baseUrl no longer
48
+ // matches its template and can't be auto-migrated, so the notice is shown
49
+ // only once rather than on every session_start while the config stays stale.
50
+ staleNotified?: boolean;
47
51
  }
48
52
 
49
53
  interface ExtensionConfig {
@@ -376,19 +380,24 @@ function escapeRegex(s: string): string {
376
380
  * treat as "this provider can't be healed automatically".
377
381
  */
378
382
  function recoverPlaceholders(templateUrl: string, savedUrl: string): Record<string, string> | null {
383
+ // Tolerate trailing-slash differences the same way the rest of the code does
384
+ // (e.g. baseUrl.replace(/\/+$/, "")), so a saved URL that differs only by a
385
+ // trailing slash still heals instead of being marked stale.
386
+ const tpl = templateUrl.replace(/\/+$/, "");
387
+ const saved = savedUrl.replace(/\/+$/, "");
379
388
  const order: string[] = [];
380
389
  const placeholderRe = new RegExp(URL_PLACEHOLDERS.join("|"), "g");
381
390
  let source = "^";
382
391
  let lastIndex = 0;
383
392
  let m: RegExpExecArray | null;
384
- while ((m = placeholderRe.exec(templateUrl)) !== null) {
385
- source += escapeRegex(templateUrl.slice(lastIndex, m.index)) + "([^/]+)";
393
+ while ((m = placeholderRe.exec(tpl)) !== null) {
394
+ source += escapeRegex(tpl.slice(lastIndex, m.index)) + "([^/]+)";
386
395
  order.push(m[0]);
387
396
  lastIndex = m.index + m[0].length;
388
397
  }
389
- source += escapeRegex(templateUrl.slice(lastIndex)) + "$";
398
+ source += escapeRegex(tpl.slice(lastIndex)) + "$";
390
399
 
391
- const match = new RegExp(source).exec(savedUrl);
400
+ const match = new RegExp(source).exec(saved);
392
401
  if (!match) return null;
393
402
  const out: Record<string, string> = {};
394
403
  order.forEach((name, i) => { out[name] = match[i + 1]; });
@@ -404,7 +413,8 @@ function applyPlaceholders(url: string, values: Record<string, string>): string
404
413
  /**
405
414
  * Backfill missing discovery fields on saved providers from their template.
406
415
  * 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.
416
+ * `healed` is non-empty. `stale` lists the provider *keys* that need a manual
417
+ * re-login (the caller resolves display names and dedupes notifications).
408
418
  */
409
419
  function migrateDiscoveryFields(config: ExtensionConfig): { healed: string[]; stale: string[] } {
410
420
  const healed: string[] = [];
@@ -416,7 +426,7 @@ function migrateDiscoveryFields(config: ExtensionConfig): { healed: string[]; st
416
426
  if (p.modelsUrl) continue; // already set (fresh login or manual edit) — never clobber
417
427
 
418
428
  const values = recoverPlaceholders(tpl.baseUrl, p.baseUrl);
419
- if (!values) { stale.push(p.displayName); continue; }
429
+ if (!values) { stale.push(key); continue; }
420
430
 
421
431
  p.modelsUrl = applyPlaceholders(tpl.modelsUrl, values);
422
432
  p.modelsIdField = tpl.modelsIdField;
@@ -493,8 +503,10 @@ async function fetchModels(
493
503
  else if (Array.isArray(obj.result)) raw = obj.result as RawModel[];
494
504
  }
495
505
  if (!raw) {
506
+ // `url` may be an override (e.g. /catalog/models, /ai/models/search), so
507
+ // keep the wording generic rather than referring specifically to /models.
496
508
  throw new Error(
497
- `Unexpected /models payload shape from ${url} ` +
509
+ `Unexpected model catalog payload shape from ${url} ` +
498
510
  `(expected an array or an object with a "data" or "result" array).`
499
511
  );
500
512
  }
@@ -506,8 +518,14 @@ async function fetchModels(
506
518
  return taskName.toLowerCase() === keepTask.toLowerCase();
507
519
  })
508
520
  .map((m) => {
509
- const id = (m as Record<string, unknown>)[idField] as string | undefined;
510
- return { id: id ?? "", contextWindow: m.context_window, maxTokens: m.max_tokens };
521
+ // Coerce the id field defensively: some upstreams expose a numeric id,
522
+ // and storing a non-string would break the localeCompare sort below.
523
+ const rawId = (m as Record<string, unknown>)[idField];
524
+ const id =
525
+ typeof rawId === "string" ? rawId :
526
+ typeof rawId === "number" ? String(rawId) :
527
+ "";
528
+ return { id, contextWindow: m.context_window, maxTokens: m.max_tokens };
511
529
  })
512
530
  .filter((m) => Boolean(m.id))
513
531
  .sort((a, b) => a.id.localeCompare(b.id));
@@ -615,12 +633,19 @@ export default async function (pi: ExtensionAPI) {
615
633
  "warning"
616
634
  );
617
635
  }
618
- if (stale.length > 0) {
636
+ // Warn about stale providers only once: re-notifying on every session_start
637
+ // while the config stays stale would be noise. Persist the flag so the
638
+ // notice survives restarts.
639
+ const toNotify = stale.filter((key) => !config.providers[key]?.staleNotified);
640
+ if (toNotify.length > 0) {
641
+ const names = toNotify.map((key) => config.providers[key].displayName);
619
642
  ctx.ui.notify(
620
- `OpenAI-compat: ${stale.join(", ")} ${stale.length === 1 ? "has" : "have"} an out-of-date ` +
643
+ `OpenAI-compat: ${names.join(", ")} ${names.length === 1 ? "has" : "have"} an out-of-date ` +
621
644
  `base URL — run /compat-login to update (its endpoint changed and can't be migrated automatically).`,
622
645
  "warning"
623
646
  );
647
+ for (const key of toNotify) config.providers[key].staleNotified = true;
648
+ saveConfig(config);
624
649
  }
625
650
  });
626
651
 
@@ -807,10 +832,17 @@ export default async function (pi: ExtensionAPI) {
807
832
  keys = providerKeys;
808
833
  } else {
809
834
  const ALL = "All providers";
810
- const labels = [ALL, ...providerKeys.map((k) => config.providers[k].displayName)];
835
+ // Embed the internal provider key in each label so duplicate
836
+ // displayNames (or a provider literally named "All providers") can't
837
+ // collide with each other or the special "All providers" option.
838
+ const options = providerKeys.map((k) => ({
839
+ key: k,
840
+ label: `${config.providers[k].displayName} [${k}]`,
841
+ }));
842
+ const labels = [ALL, ...options.map((o) => o.label)];
811
843
  const chosen = await ctx.ui.select("Refresh which provider?", labels);
812
844
  if (!chosen) { ctx.ui.notify("Cancelled.", "info"); return; }
813
- keys = chosen === ALL ? providerKeys : [providerKeys[labels.indexOf(chosen) - 1]];
845
+ keys = chosen === ALL ? providerKeys : [options[labels.indexOf(chosen) - 1].key];
814
846
  }
815
847
 
816
848
  const refreshed: string[] = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@billjr99/pi-openai-compat",
3
- "version": "1.1.14",
3
+ "version": "1.1.15",
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",