@billjr99/pi-openai-compat 1.1.11 → 1.1.13

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 +47 -12
  2. package/index.ts +202 -17
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -49,30 +49,40 @@ If pi is already running when you install, type `/reload` first.
49
49
  | **Venice AI** | `https://api.venice.ai/api/v1` | API key from venice.ai/settings/api |
50
50
  | **Fireworks AI** | `https://api.fireworks.ai/inference/v1` | API key from fireworks.ai/account/api-keys |
51
51
  | **Together AI** | `https://api.together.xyz/v1` | API key from api.together.ai/settings/api-keys |
52
- | **Cloudflare Workers AI** | `https://api.cloudflare.com/client/v4/accounts/{account}/ai/v1` | API token from dash.cloudflare.com |
53
- | **Cloudflare AI Gateway** | `https://gateway.ai.cloudflare.com/v1/{account}/{gateway}/openai` | API token from dash.cloudflare.com |
52
+ | **GitHub Models** | `https://models.github.ai/inference` | GitHub PAT fine-grained, `Models → read` |
53
+ | **Cloudflare Workers AI** | `https://api.cloudflare.com/client/v4/accounts/{account}/ai/v1` | API token from dash.cloudflare.com (`Workers AI: Read`) |
54
+ | **Cloudflare AI Gateway** | `https://gateway.ai.cloudflare.com/v1/{account}/{gateway}/openai` | API token from dash.cloudflare.com (`AI Gateway: Run` + `Workers AI: Read`) |
54
55
  | **Vercel AI Gateway** | `https://ai-gateway.vercel.sh/v1` | API key from vercel.com |
55
56
  | **OpenCode Zen** | `https://opencode.ai/zen/v1` | API key from opencode.ai |
56
57
  | **Ollama (local)** | `http://localhost:11434/v1` | Keyless |
57
58
  | **Ollama Cloud** | `https://ollama.com/v1` | Ollama Cloud API key from ollama.com |
58
59
  | **Custom** | Any URL you supply | Optional bearer token |
59
60
 
60
- > **Providers with built-in fallback model lists (as of May 2026)**
61
- > Some providers do not support `GET /v1/models` or return a non-JSON response.
62
- > For these, the extension substitutes a built-in list and shows a warning during
63
- > `/compat-login`.
61
+ > **Providers whose model catalog lives at a non-standard `/models` path (as of June 2026)**
62
+ > Some providers don't return models at `<base_url>/models`. The extension
63
+ > handles them in one of two ways:
64
64
  >
65
- > | Provider | Reason |
66
- > |---|---|
67
- > | **Cloudflare Workers AI** | Returns HTTP 405 method not supported |
68
- > | **Cloudflare AI Gateway** | Returns HTTP 401 — no anonymous model enumeration |
69
- > | **Hugging Face** | Returns HTML rather than JSON for `/v1/models` |
65
+ > 1. **Live discovery via a per-template URL override** (`modelsUrl`,
66
+ > `modelsIdField`, `modelsKeepTask` on the template). Used for **GitHub
67
+ > Models** and **Cloudflare Workers AI** — `/compat-login` fetches the real
68
+ > catalog and registers actual current models, not a curated guess.
69
+ > 2. **Built-in fallback list** when no working catalog endpoint exists. Used
70
+ > when discovery fails or when the upstream simply has no `/models` at all
71
+ > (e.g. **Hugging Face** returns HTML, **Cloudflare AI Gateway** has no
72
+ > catalog endpoint).
73
+ >
74
+ > | Provider | Default `/models` symptom | Handling |
75
+ > |---|---|---|
76
+ > | **GitHub Models** | HTTP 404 at `<base_url>/models` (catalog is at `/catalog/models`, not `/inference/models`) | Live discovery — `modelsUrl: https://models.github.ai/catalog/models` |
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
+ > | **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
+ > | **Hugging Face** | Returns HTML rather than JSON | Built-in fallback list |
70
80
 
71
81
  ---
72
82
 
73
83
  ## Commands
74
84
 
75
- Only two commands are needed.
85
+ Three commands are available; `/compat-login` is the only one you need to get started.
76
86
 
77
87
  ### `/compat-login`
78
88
 
@@ -88,6 +98,18 @@ After login, the provider's models appear in pi's `/model` command and
88
98
  `Ctrl+L` picker immediately. You can run `/compat-login` again to add a
89
99
  second provider — all providers are active simultaneously.
90
100
 
101
+ ### `/compat-refresh`
102
+
103
+ Re-fetches the model list for an already-registered provider, reusing the
104
+ saved base URL, API key, and discovery settings — no need to re-enter URLs,
105
+ keys, or account IDs. Use it to pick up models a provider has added (or
106
+ dropped) since you logged in, without restarting your session.
107
+
108
+ If you have multiple providers registered, you are asked which one to refresh
109
+ (or choose **All providers**); with a single provider it refreshes directly.
110
+ A failed or empty refresh leaves the existing model list untouched, so a flaky
111
+ network call can't blank out a working provider.
112
+
91
113
  ### `/compat-logout`
92
114
 
93
115
  Unregisters a provider from pi. If you have multiple providers registered,
@@ -272,6 +294,19 @@ pi remove npm:@billjr99/pi-openai-compat # uninstall
272
294
  Verify the base URL does not have a trailing slash and ends with `/v1`.
273
295
  Confirm the API key is correct and has model-access permissions.
274
296
  For Ollama: ensure `ollama serve` is running.
297
+ For self-hosted Open WebUI: the OpenAI-compatible API is under `/api/v1`, not
298
+ the SPA root — e.g. `https://your-host/api/v1`, not `https://your-host`.
299
+ Hitting the root returns the HTML SPA and `/compat-login` fails with a JSON
300
+ parse error.
301
+
302
+ **Cloudflare AI Gateway returns 401 Unauthorized**
303
+ Check both: (1) the API token has `AI Gateway: Run` *and* `Workers AI: Read`
304
+ under "Permissions", scoped to the correct account; and (2) the gateway slug
305
+ in the URL actually exists under that account — list with
306
+ `curl https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/ai-gateway/gateways
307
+ -H "Authorization: Bearer $TOKEN"`. An empty `result` array means the gateway
308
+ isn't there (you may need to create it in **dash.cloudflare.com → AI → AI
309
+ Gateway**, or you're querying the wrong account).
275
310
 
276
311
  **No models appear after login**
277
312
  For Ollama: pull at least one model first (`ollama pull llama3`).
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 adds a provider (fetches fresh model list, registers).
12
- * - /compat-logout removes a provider (unregisters, restores previous model).
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
@@ -35,6 +36,14 @@ interface ProviderConfig {
35
36
  baseUrl: string;
36
37
  apiKey: string | null;
37
38
  cachedModels: CachedModel[];
39
+ // Optional model-discovery overrides for providers whose /models endpoint
40
+ // lives at a non-standard path / shape (e.g. GitHub Models' /catalog/models,
41
+ // Cloudflare Workers AI's /ai/models/search). Persisted on the provider so
42
+ // session_start re-fetches (when cachedModels is empty) use the override
43
+ // rather than the broken default <baseUrl>/models path.
44
+ modelsUrl?: string;
45
+ modelsIdField?: string;
46
+ modelsKeepTask?: string;
38
47
  }
39
48
 
40
49
  interface ExtensionConfig {
@@ -48,6 +57,15 @@ interface OpenAIModelsResponse {
48
57
  data: Array<{ id: string; context_window?: number; max_tokens?: number }>;
49
58
  }
50
59
 
60
+ /** Loose shape for a single entry in any /models response. */
61
+ type RawModel = {
62
+ id?: string;
63
+ name?: string;
64
+ context_window?: number;
65
+ max_tokens?: number;
66
+ task?: { name?: string };
67
+ };
68
+
51
69
  // ─────────────────────────────────────────────────────────────────────────────
52
70
  // Provider templates
53
71
  // ─────────────────────────────────────────────────────────────────────────────
@@ -64,6 +82,17 @@ const TEMPLATES: Record<string, {
64
82
  fallbackModels?: string[];
65
83
  /** Where to obtain the API key; presence implies the key is required. */
66
84
  keyHint?: string;
85
+ /**
86
+ * Optional model-discovery overrides for providers whose /models endpoint
87
+ * lives at a non-standard path / shape. May contain the same placeholders
88
+ * as baseUrl (YOUR_ACCOUNT_ID, YOUR_GATEWAY_SLUG, YOUR_PROVIDER); they are
89
+ * substituted alongside the baseUrl substitution in /compat-login.
90
+ */
91
+ modelsUrl?: string;
92
+ /** Field on each model entry that carries the upstream id (default "id"). */
93
+ modelsIdField?: string;
94
+ /** Keep only models whose task.name matches this string (case-insensitive). */
95
+ modelsKeepTask?: string;
67
96
  }> = {
68
97
  openrouter: {
69
98
  displayName: "OpenRouter",
@@ -97,9 +126,13 @@ const TEMPLATES: Record<string, {
97
126
  },
98
127
  github_models: {
99
128
  displayName: "GitHub Models",
100
- baseUrl: "https://models.inference.ai.azure.com",
129
+ // The legacy Azure endpoint (models.inference.ai.azure.com) was retired.
130
+ // Chat now lives under /inference, and the catalog under /catalog/models
131
+ // on the same host — different path entirely, hence the modelsUrl override.
132
+ baseUrl: "https://models.github.ai/inference",
101
133
  keyless: false,
102
- keyHint: "github.com/settings/tokens",
134
+ keyHint: "github.com/settings/tokens (fine-grained: Models → read)",
135
+ modelsUrl: "https://models.github.ai/catalog/models",
103
136
  },
104
137
  sambanova: {
105
138
  displayName: "SambaNova",
@@ -124,8 +157,14 @@ const TEMPLATES: Record<string, {
124
157
  baseUrl: "https://api.cloudflare.com/client/v4/accounts/YOUR_ACCOUNT_ID/ai/v1",
125
158
  keyless: false,
126
159
  promptUrl: true,
127
- keyHint: "dash.cloudflare.com → My Profile → API Tokens",
128
- // Cloudflare Workers AI returns 405 for GET /v1/models; use a curated list.
160
+ keyHint: "dash.cloudflare.com → My Profile → API Tokens (`Workers AI: Read`)",
161
+ // The OpenAI-compat base /ai/v1 returns 405 for GET /models. The real
162
+ // catalog lives at /ai/models/search, keys ids in "name" (reserving "id"
163
+ // for an internal UUID), and mixes Text Generation with embeddings and
164
+ // image tasks — so we filter by task.name.
165
+ modelsUrl: "https://api.cloudflare.com/client/v4/accounts/YOUR_ACCOUNT_ID/ai/models/search?per_page=100",
166
+ modelsIdField: "name",
167
+ modelsKeepTask: "Text Generation",
129
168
  fallbackModels: [
130
169
  "@cf/meta/llama-4-scout-17b-16e-instruct",
131
170
  "@cf/meta/llama-3.3-70b-instruct-fp8-fast",
@@ -138,10 +177,16 @@ const TEMPLATES: Record<string, {
138
177
  displayName: "Cloudflare AI Gateway",
139
178
  // YOUR_PROVIDER is the upstream slug (e.g. "workers-ai", "openai"). /v1 is
140
179
  // appended so that fetchModels and chat completions hit the correct path.
180
+ // Common failure modes:
181
+ // - 401 "Unauthorized": the token lacks `AI Gateway: Run` permission,
182
+ // or the gateway has Authenticated Gateway enabled (requires a
183
+ // separate cf-aig-authorization header, not yet supported here).
184
+ // - 400 "Please configure AI Gateway": the gateway slug doesn't exist
185
+ // under this account, or the upstream provider isn't configured on it.
141
186
  baseUrl: "https://gateway.ai.cloudflare.com/v1/YOUR_ACCOUNT_ID/YOUR_GATEWAY_SLUG/YOUR_PROVIDER/v1",
142
187
  keyless: false,
143
188
  promptUrl: true,
144
- keyHint: "dash.cloudflare.com → My Profile → API Tokens",
189
+ keyHint: "dash.cloudflare.com → My Profile → API Tokens (`AI Gateway: Run` + `Workers AI: Read`)",
145
190
  fallbackModels: [
146
191
  "@cf/meta/llama-4-scout-17b-16e-instruct",
147
192
  "@cf/meta/llama-3.3-70b-instruct-fp8-fast",
@@ -323,22 +368,69 @@ function isLocalUrl(url: string): boolean {
323
368
  }
324
369
  }
325
370
 
326
- async function fetchModels(baseUrl: string, apiKey: string | null): Promise<CachedModel[]> {
327
- const url = `${baseUrl.replace(/\/+$/, "")}/models`;
371
+ /** Optional per-provider overrides controlling how /models is fetched. */
372
+ interface FetchOverrides {
373
+ /** Full URL to fetch instead of `<baseUrl>/models`. */
374
+ url?: string;
375
+ /** Field on each entry that holds the upstream id (default `id`). */
376
+ idField?: string;
377
+ /** Keep only entries whose `task.name` matches (case-insensitive). */
378
+ keepTask?: string;
379
+ }
380
+
381
+ async function fetchModels(
382
+ baseUrl: string,
383
+ apiKey: string | null,
384
+ overrides: FetchOverrides = {},
385
+ ): Promise<CachedModel[]> {
386
+ // Honor a per-provider override (e.g. GitHub Models' /catalog/models lives
387
+ // on a different path than its inference endpoint; Cloudflare Workers AI's
388
+ // catalog is at /ai/models/search). Fall back to <baseUrl>/models otherwise.
389
+ const url = overrides.url ?? `${baseUrl.replace(/\/+$/, "")}/models`;
390
+ const idField = overrides.idField ?? "id";
391
+ const keepTask = overrides.keepTask;
392
+
328
393
  const headers: Record<string, string> = { Accept: "application/json" };
329
394
  if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
330
395
 
331
396
  const resp = await fetch(url, { headers });
332
397
  if (!resp.ok) {
398
+ // The Authorization header is never echoed here, so the error body is
399
+ // safe to surface even though we include the upstream's full response.
333
400
  const body = await resp.text().catch(() => "");
334
401
  throw new Error(`HTTP ${resp.status} from ${url}: ${body}`);
335
402
  }
336
403
 
337
- const json = (await resp.json()) as OpenAIModelsResponse;
338
- if (!Array.isArray(json?.data)) throw new Error(`Unexpected response from ${url}`);
404
+ // Normalize the various shapes /models can return:
405
+ // - OpenAI style: {"data": [...]}
406
+ // - Cloudflare / some gateways: {"result": [...]}
407
+ // - Together (and a few others): [...] (bare JSON array)
408
+ const json = (await resp.json()) as unknown;
409
+ let raw: RawModel[] | undefined;
410
+ if (Array.isArray(json)) {
411
+ raw = json as RawModel[];
412
+ } else if (json && typeof json === "object") {
413
+ const obj = json as { data?: unknown; result?: unknown };
414
+ if (Array.isArray(obj.data)) raw = obj.data as RawModel[];
415
+ else if (Array.isArray(obj.result)) raw = obj.result as RawModel[];
416
+ }
417
+ if (!raw) {
418
+ throw new Error(
419
+ `Unexpected /models payload shape from ${url} ` +
420
+ `(expected an array or an object with a "data" or "result" array).`
421
+ );
422
+ }
339
423
 
340
- return json.data
341
- .map((m) => ({ id: m.id, contextWindow: m.context_window, maxTokens: m.max_tokens }))
424
+ return raw
425
+ .filter((m) => {
426
+ if (keepTask === undefined) return true;
427
+ const taskName = m.task?.name ?? "";
428
+ return taskName.toLowerCase() === keepTask.toLowerCase();
429
+ })
430
+ .map((m) => {
431
+ const id = (m as Record<string, unknown>)[idField] as string | undefined;
432
+ return { id: id ?? "", contextWindow: m.context_window, maxTokens: m.max_tokens };
433
+ })
342
434
  .filter((m) => Boolean(m.id))
343
435
  .sort((a, b) => a.id.localeCompare(b.id));
344
436
  }
@@ -404,9 +496,14 @@ export default async function (pi: ExtensionAPI) {
404
496
  registerProvider(pi, key, p);
405
497
  registered.push(p.displayName);
406
498
  } else {
407
- // Cache is empty (e.g. migrated from older config). Try a live fetch.
499
+ // Cache is empty (e.g. migrated from older config). Try a live fetch,
500
+ // honoring any per-provider discovery overrides stored on the config.
408
501
  try {
409
- const models = await fetchModels(p.baseUrl, p.apiKey);
502
+ const models = await fetchModels(p.baseUrl, p.apiKey, {
503
+ url: p.modelsUrl,
504
+ idField: p.modelsIdField,
505
+ keepTask: p.modelsKeepTask,
506
+ });
410
507
  if (models.length > 0) {
411
508
  p.cachedModels = models;
412
509
  saveConfig(config);
@@ -456,6 +553,11 @@ export default async function (pi: ExtensionAPI) {
456
553
 
457
554
  // Step 2 — base URL
458
555
  let baseUrl = tpl.baseUrl;
556
+ // modelsUrl tracks the discovery URL through the same placeholder
557
+ // substitutions baseUrl goes through, so providers that put the model
558
+ // catalog on a different path (GitHub Models, Cloudflare Workers AI)
559
+ // get a fully-resolved URL by the time we call fetchModels.
560
+ let modelsUrl: string | undefined = tpl.modelsUrl;
459
561
  if (key === "cloudflare_workers") {
460
562
  const entered = await ctx.ui.input(
461
563
  "Account ID",
@@ -466,6 +568,7 @@ export default async function (pi: ExtensionAPI) {
466
568
  const accountId = entered.trim();
467
569
  if (!accountId) { ctx.ui.notify("Account ID cannot be empty.", "error"); return; }
468
570
  baseUrl = tpl.baseUrl.replace("YOUR_ACCOUNT_ID", accountId);
571
+ if (modelsUrl) modelsUrl = modelsUrl.replace("YOUR_ACCOUNT_ID", accountId);
469
572
  } else if (key === "cloudflare_ai_gateway") {
470
573
  const accountIdInput = await ctx.ui.input(
471
574
  "Account ID",
@@ -497,6 +600,12 @@ export default async function (pi: ExtensionAPI) {
497
600
  .replace("YOUR_ACCOUNT_ID", accountId)
498
601
  .replace("YOUR_GATEWAY_SLUG", gatewaySlug)
499
602
  .replace("YOUR_PROVIDER", provider);
603
+ if (modelsUrl) {
604
+ modelsUrl = modelsUrl
605
+ .replace("YOUR_ACCOUNT_ID", accountId)
606
+ .replace("YOUR_GATEWAY_SLUG", gatewaySlug)
607
+ .replace("YOUR_PROVIDER", provider);
608
+ }
500
609
  } else if (tpl.promptUrl) {
501
610
  const defaultUrl = tpl.baseUrl;
502
611
  const prompt = isLocalUrl(defaultUrl)
@@ -523,7 +632,11 @@ export default async function (pi: ExtensionAPI) {
523
632
  ctx.ui.notify(`Connecting to ${baseUrl} …`, "info");
524
633
  let models: CachedModel[];
525
634
  try {
526
- models = await fetchModels(baseUrl, apiKey);
635
+ models = await fetchModels(baseUrl, apiKey, {
636
+ url: modelsUrl,
637
+ idField: tpl.modelsIdField,
638
+ keepTask: tpl.modelsKeepTask,
639
+ });
527
640
  } catch (err) {
528
641
  if (tpl.fallbackModels && tpl.fallbackModels.length > 0) {
529
642
  ctx.ui.notify(
@@ -555,12 +668,18 @@ export default async function (pi: ExtensionAPI) {
555
668
  }
556
669
  }
557
670
 
558
- // Step 5 — save to config and register with pi
671
+ // Step 5 — save to config and register with pi.
672
+ // The discovery overrides are persisted so session_start can re-fetch
673
+ // correctly when cachedModels is empty (without them, the rehydrate
674
+ // path would hit <baseUrl>/models and 404 for these providers).
559
675
  config.providers[key] = {
560
676
  displayName: tpl.displayName,
561
677
  baseUrl,
562
678
  apiKey,
563
679
  cachedModels: models,
680
+ modelsUrl,
681
+ modelsIdField: tpl.modelsIdField,
682
+ modelsKeepTask: tpl.modelsKeepTask,
564
683
  };
565
684
  saveConfig(config);
566
685
  registerProvider(pi, key, config.providers[key]);
@@ -572,6 +691,72 @@ export default async function (pi: ExtensionAPI) {
572
691
  },
573
692
  });
574
693
 
694
+ // ── /compat-refresh ──────────────────────────────────────────────────────────
695
+ // Force a re-fetch of an already-registered provider's model list, reusing
696
+ // the persisted baseUrl/apiKey and discovery overrides — no need to re-enter
697
+ // URLs, keys, or account IDs like /compat-login. Picks up newly added (or
698
+ // dropped) upstream models without restarting the session.
699
+ pi.registerCommand("compat-refresh", {
700
+ description: "Re-fetch the model list for a registered OpenAI-compatible provider",
701
+ handler: async (_args, ctx) => {
702
+ const providerKeys = Object.keys(config.providers);
703
+ if (!providerKeys.length) {
704
+ ctx.ui.notify("No compat providers are registered. Run /compat-login first.", "info");
705
+ return;
706
+ }
707
+
708
+ // Choose which provider(s) to refresh. With one provider, refresh it
709
+ // directly; otherwise offer each by name plus an "All providers" option.
710
+ let keys: string[];
711
+ if (providerKeys.length === 1) {
712
+ keys = providerKeys;
713
+ } else {
714
+ const ALL = "All providers";
715
+ const labels = [ALL, ...providerKeys.map((k) => config.providers[k].displayName)];
716
+ const chosen = await ctx.ui.select("Refresh which provider?", labels);
717
+ if (!chosen) { ctx.ui.notify("Cancelled.", "info"); return; }
718
+ keys = chosen === ALL ? providerKeys : [providerKeys[labels.indexOf(chosen) - 1]];
719
+ }
720
+
721
+ const refreshed: string[] = [];
722
+ const failed: string[] = [];
723
+ for (const key of keys) {
724
+ const p = config.providers[key];
725
+ ctx.ui.notify(`Refreshing ${p.displayName} …`, "info");
726
+ try {
727
+ const models = await fetchModels(p.baseUrl, p.apiKey, {
728
+ url: p.modelsUrl,
729
+ idField: p.modelsIdField,
730
+ keepTask: p.modelsKeepTask,
731
+ });
732
+ if (models.length > 0) {
733
+ // Only overwrite the cache on a successful, non-empty fetch — a
734
+ // flaky refresh must never blank out a working provider's models.
735
+ p.cachedModels = models;
736
+ saveConfig(config);
737
+ registerProvider(pi, key, p);
738
+ refreshed.push(`${p.displayName} (${models.length})`);
739
+ } else {
740
+ failed.push(p.displayName);
741
+ }
742
+ } catch {
743
+ failed.push(p.displayName);
744
+ }
745
+ }
746
+
747
+ if (refreshed.length > 0) {
748
+ ctx.ui.notify(`Refreshed: ${refreshed.join(", ")}.`, "success");
749
+ }
750
+ if (failed.length > 0) {
751
+ ctx.ui.notify(
752
+ `Could not refresh ${failed.join(", ")} — kept the existing model list. ` +
753
+ `Run /compat-login if the provider's URL or key changed.`,
754
+ "warning"
755
+ );
756
+ }
757
+ },
758
+ });
759
+
575
760
  // ── /compat-logout ─────────────────────────────────────────────────────────
576
761
  pi.registerCommand("compat-logout", {
577
762
  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.11",
3
+ "version": "1.1.13",
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",