@billjr99/pi-openai-compat 1.0.2 → 1.1.9
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 +82 -0
- package/index.ts +177 -55
- package/package.json +8 -4
package/README.md
CHANGED
|
@@ -40,9 +40,32 @@ If pi is already running when you install, type `/reload` first.
|
|
|
40
40
|
| **OpenRouter** | `https://openrouter.ai/api/v1` | `sk-or-...` from openrouter.ai/keys |
|
|
41
41
|
| **NVIDIA NIM** | `https://integrate.api.nvidia.com/v1` | `nvapi-...` from build.nvidia.com |
|
|
42
42
|
| **Nous Research Portal** | `https://inference-api.nousresearch.com/v1` | Nous Portal API key |
|
|
43
|
+
| **DeepSeek** | `https://api.deepseek.com/v1` | API key from platform.deepseek.com |
|
|
44
|
+
| **xAI (Grok)** | `https://api.x.ai/v1` | API key from console.x.ai |
|
|
45
|
+
| **Hugging Face** | `https://router.huggingface.co/v1` | `hf_...` token from huggingface.co/settings/tokens |
|
|
46
|
+
| **Moonshot (Kimi)** | `https://api.moonshot.ai/v1` | `sk-...` key from platform.moonshot.ai |
|
|
47
|
+
| **MiniMax** | `https://api.minimax.io/v1` | API key from platform.minimax.io |
|
|
48
|
+
| **Z.ai** | `https://api.z.ai/api/paas/v4` | API key from z.ai |
|
|
49
|
+
| **Venice AI** | `https://api.venice.ai/api/v1` | API key from venice.ai/settings/api |
|
|
50
|
+
| **Cloudflare Workers AI** | `https://api.cloudflare.com/client/v4/accounts/{account}/ai/v1` | API token from dash.cloudflare.com |
|
|
51
|
+
| **Cloudflare AI Gateway** | `https://gateway.ai.cloudflare.com/v1/{account}/{gateway}/openai` | API token from dash.cloudflare.com |
|
|
52
|
+
| **Vercel AI Gateway** | `https://ai-gateway.vercel.sh/v1` | API key from vercel.com |
|
|
53
|
+
| **OpenCode Zen** | `https://opencode.ai/zen/v1` | API key from opencode.ai |
|
|
43
54
|
| **Ollama (local)** | `http://localhost:11434/v1` | Keyless |
|
|
55
|
+
| **Ollama Cloud** | `https://ollama.com/v1` | Ollama Cloud API key from ollama.com |
|
|
44
56
|
| **Custom** | Any URL you supply | Optional bearer token |
|
|
45
57
|
|
|
58
|
+
> **Providers with built-in fallback model lists (as of May 2026)**
|
|
59
|
+
> Some providers do not support `GET /v1/models` or return a non-JSON response.
|
|
60
|
+
> For these, the extension substitutes a built-in list and shows a warning during
|
|
61
|
+
> `/compat-login`.
|
|
62
|
+
>
|
|
63
|
+
> | Provider | Reason |
|
|
64
|
+
> |---|---|
|
|
65
|
+
> | **Cloudflare Workers AI** | Returns HTTP 405 — method not supported |
|
|
66
|
+
> | **Cloudflare AI Gateway** | Returns HTTP 401 — no anonymous model enumeration |
|
|
67
|
+
> | **Hugging Face** | Returns HTML rather than JSON for `/v1/models` |
|
|
68
|
+
|
|
46
69
|
---
|
|
47
70
|
|
|
48
71
|
## Commands
|
|
@@ -171,6 +194,65 @@ npm publish --access public
|
|
|
171
194
|
|
|
172
195
|
`npm version` also creates a git tag, so GitHub gets release tags automatically.
|
|
173
196
|
|
|
197
|
+
### Automated publishing via GitHub Actions
|
|
198
|
+
|
|
199
|
+
Every push to `main` automatically bumps the patch version and publishes to npm
|
|
200
|
+
via `.github/workflows/publish.yml`. This workflow uses **npm trusted publishing**
|
|
201
|
+
(OIDC) — no long-lived token or secret is needed anywhere.
|
|
202
|
+
|
|
203
|
+
#### Required one-time setup on npmjs.com
|
|
204
|
+
|
|
205
|
+
> **Do this before the first automated publish or it will fail.**
|
|
206
|
+
|
|
207
|
+
1. Go to **npmjs.com** → sign in → click your avatar → **Packages** →
|
|
208
|
+
`@billjr99/pi-openai-compat`
|
|
209
|
+
2. Click **Settings** (in the left sidebar of the package page)
|
|
210
|
+
3. Scroll to **Publishing** → enable **Trusted Publishing**
|
|
211
|
+
4. Add a publisher with these exact values:
|
|
212
|
+
|
|
213
|
+
| Field | Value |
|
|
214
|
+
|---|---|
|
|
215
|
+
| Repository owner | `BillJr99` |
|
|
216
|
+
| Repository name | `pi-openai-compat` |
|
|
217
|
+
| Workflow filename | `publish.yml` |
|
|
218
|
+
| Environment | *(leave blank)* |
|
|
219
|
+
|
|
220
|
+
5. Click **Save**
|
|
221
|
+
|
|
222
|
+
No token to copy, no secret to rotate. That's the only setup step.
|
|
223
|
+
|
|
224
|
+
#### What the workflow does
|
|
225
|
+
|
|
226
|
+
```yaml
|
|
227
|
+
# .github/workflows/publish.yml
|
|
228
|
+
on:
|
|
229
|
+
push:
|
|
230
|
+
branches: [main]
|
|
231
|
+
|
|
232
|
+
permissions:
|
|
233
|
+
contents: write # push the version-bump commit back to main
|
|
234
|
+
id-token: write # request an OIDC token from GitHub for npm auth
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
On every push to `main` the workflow:
|
|
238
|
+
|
|
239
|
+
1. Checks out the repo with `GITHUB_TOKEN` so it can push back
|
|
240
|
+
2. Runs `npm version patch --no-git-tag-version` to increment the patch number
|
|
241
|
+
in `package.json` (e.g. `1.1.0` → `1.1.1`)
|
|
242
|
+
3. Commits the updated `package.json` with `[skip ci]` in the message so the
|
|
243
|
+
commit does not re-trigger the workflow
|
|
244
|
+
4. Pushes the bump commit to `main`
|
|
245
|
+
5. Runs `npm publish --access public --provenance`
|
|
246
|
+
- GitHub mints a short-lived OIDC token proving the job is running from
|
|
247
|
+
this exact repo and workflow
|
|
248
|
+
- npm validates it against the trusted publisher config above and allows
|
|
249
|
+
the publish — no `NPM_TOKEN` secret required
|
|
250
|
+
- `--provenance` attaches a signed build attestation to the package,
|
|
251
|
+
visible on the npmjs.com package page under **Provenance**
|
|
252
|
+
|
|
253
|
+
The published package is immediately available at
|
|
254
|
+
https://www.npmjs.com/package/@billjr99/pi-openai-compat.
|
|
255
|
+
|
|
174
256
|
### Installing from npm
|
|
175
257
|
|
|
176
258
|
```bash
|
package/index.ts
CHANGED
|
@@ -60,111 +60,155 @@ const TEMPLATES: Record<string, {
|
|
|
60
60
|
modelFilter?: string[];
|
|
61
61
|
/** If true, prompt the user to confirm/edit the base URL (like ollama/custom). */
|
|
62
62
|
promptUrl?: boolean;
|
|
63
|
+
/** Used when the provider does not support GET /v1/models (e.g. returns 405). */
|
|
64
|
+
fallbackModels?: string[];
|
|
65
|
+
/** Where to obtain the API key; presence implies the key is required. */
|
|
66
|
+
keyHint?: string;
|
|
63
67
|
}> = {
|
|
64
68
|
openrouter: {
|
|
65
69
|
displayName: "OpenRouter",
|
|
66
70
|
baseUrl: "https://openrouter.ai/api/v1",
|
|
67
71
|
keyless: false,
|
|
72
|
+
keyHint: "openrouter.ai/keys",
|
|
68
73
|
},
|
|
69
74
|
nvidia_nim: {
|
|
70
75
|
displayName: "NVIDIA NIM",
|
|
71
76
|
baseUrl: "https://integrate.api.nvidia.com/v1",
|
|
72
77
|
keyless: false,
|
|
78
|
+
keyHint: "build.nvidia.com",
|
|
73
79
|
},
|
|
74
80
|
nous: {
|
|
75
81
|
displayName: "Nous Research Portal",
|
|
76
82
|
baseUrl: "https://inference-api.nousresearch.com/v1",
|
|
77
83
|
keyless: false,
|
|
84
|
+
keyHint: "nousresearch.com",
|
|
78
85
|
},
|
|
79
86
|
google: {
|
|
80
87
|
displayName: "Google Gemini",
|
|
81
88
|
baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai",
|
|
82
89
|
keyless: false,
|
|
83
|
-
|
|
84
|
-
"gemini-2.5-flash",
|
|
85
|
-
"gemini-2.5-flash-lite",
|
|
86
|
-
"gemini-3-flash-preview",
|
|
87
|
-
"gemini-3.1-flash-lite-preview",
|
|
88
|
-
"gemini-3.1-pro-preview",
|
|
89
|
-
],
|
|
90
|
+
keyHint: "aistudio.google.com/apikey",
|
|
90
91
|
},
|
|
91
92
|
cerebras: {
|
|
92
93
|
displayName: "Cerebras",
|
|
93
94
|
baseUrl: "https://api.cerebras.ai/v1",
|
|
94
95
|
keyless: false,
|
|
95
|
-
|
|
96
|
+
keyHint: "cloud.cerebras.ai",
|
|
96
97
|
},
|
|
97
98
|
github_models: {
|
|
98
99
|
displayName: "GitHub Models",
|
|
99
100
|
baseUrl: "https://models.inference.ai.azure.com",
|
|
100
101
|
keyless: false,
|
|
101
|
-
|
|
102
|
+
keyHint: "github.com/settings/tokens",
|
|
102
103
|
},
|
|
103
104
|
sambanova: {
|
|
104
105
|
displayName: "SambaNova",
|
|
105
106
|
baseUrl: "https://api.sambanova.ai/v1",
|
|
106
107
|
keyless: false,
|
|
107
|
-
|
|
108
|
-
"Meta-Llama-3.3-70B-Instruct",
|
|
109
|
-
"DeepSeek-V3.1",
|
|
110
|
-
"DeepSeek-V3.2",
|
|
111
|
-
"gpt-oss-120b",
|
|
112
|
-
"Llama-4-Maverick-17B-128E-Instruct",
|
|
113
|
-
"gemma-3-12b-it",
|
|
114
|
-
],
|
|
108
|
+
keyHint: "cloud.sambanova.ai",
|
|
115
109
|
},
|
|
116
110
|
mistral: {
|
|
117
111
|
displayName: "Mistral",
|
|
118
112
|
baseUrl: "https://api.mistral.ai/v1",
|
|
119
113
|
keyless: false,
|
|
120
|
-
|
|
121
|
-
"mistral-large-latest",
|
|
122
|
-
"mistral-medium-latest",
|
|
123
|
-
"magistral-medium-latest",
|
|
124
|
-
"codestral-latest",
|
|
125
|
-
"devstral-latest",
|
|
126
|
-
],
|
|
114
|
+
keyHint: "console.mistral.ai/api-keys",
|
|
127
115
|
},
|
|
128
116
|
groq: {
|
|
129
117
|
displayName: "Groq",
|
|
130
118
|
baseUrl: "https://api.groq.com/openai/v1",
|
|
131
119
|
keyless: false,
|
|
132
|
-
|
|
133
|
-
"llama-3.3-70b-versatile",
|
|
134
|
-
"meta-llama/llama-4-scout-17b-16e-instruct",
|
|
135
|
-
"openai/gpt-oss-120b",
|
|
136
|
-
"openai/gpt-oss-20b",
|
|
137
|
-
"qwen/qwen3-32b",
|
|
138
|
-
"llama-3.1-8b-instant",
|
|
139
|
-
],
|
|
120
|
+
keyHint: "console.groq.com/keys",
|
|
140
121
|
},
|
|
141
|
-
|
|
122
|
+
cloudflare_workers: {
|
|
142
123
|
displayName: "Cloudflare Workers AI",
|
|
143
124
|
baseUrl: "https://api.cloudflare.com/client/v4/accounts/YOUR_ACCOUNT_ID/ai/v1",
|
|
144
125
|
keyless: false,
|
|
145
126
|
promptUrl: true,
|
|
146
|
-
|
|
127
|
+
keyHint: "dash.cloudflare.com → My Profile → API Tokens",
|
|
128
|
+
// Cloudflare Workers AI returns 405 for GET /v1/models; use a curated list.
|
|
129
|
+
fallbackModels: [
|
|
130
|
+
"@cf/meta/llama-4-scout-17b-16e-instruct",
|
|
147
131
|
"@cf/meta/llama-3.3-70b-instruct-fp8-fast",
|
|
132
|
+
"@cf/meta/llama-3.1-8b-instruct",
|
|
133
|
+
"@cf/qwen/qwen3-30b-a3b-fp8",
|
|
134
|
+
"@cf/deepseek-ai/deepseek-r1-distill-qwen-32b",
|
|
135
|
+
],
|
|
136
|
+
},
|
|
137
|
+
cloudflare_ai_gateway: {
|
|
138
|
+
displayName: "Cloudflare AI Gateway",
|
|
139
|
+
// YOUR_PROVIDER is the upstream slug (e.g. "workers-ai", "openai"). /v1 is
|
|
140
|
+
// appended so that fetchModels and chat completions hit the correct path.
|
|
141
|
+
baseUrl: "https://gateway.ai.cloudflare.com/v1/YOUR_ACCOUNT_ID/YOUR_GATEWAY_SLUG/YOUR_PROVIDER/v1",
|
|
142
|
+
keyless: false,
|
|
143
|
+
promptUrl: true,
|
|
144
|
+
keyHint: "dash.cloudflare.com → My Profile → API Tokens",
|
|
145
|
+
fallbackModels: [
|
|
148
146
|
"@cf/meta/llama-4-scout-17b-16e-instruct",
|
|
149
|
-
"@cf/
|
|
150
|
-
"@cf/
|
|
151
|
-
"@cf/moonshotai/kimi-k2.5",
|
|
152
|
-
"@cf/moonshotai/kimi-k2.6",
|
|
147
|
+
"@cf/meta/llama-3.3-70b-instruct-fp8-fast",
|
|
148
|
+
"@cf/meta/llama-3.1-8b-instruct",
|
|
153
149
|
"@cf/qwen/qwen3-30b-a3b-fp8",
|
|
154
150
|
"@cf/deepseek-ai/deepseek-r1-distill-qwen-32b",
|
|
155
|
-
"@cf/ibm-granite/granite-4.0-h-micro",
|
|
156
151
|
],
|
|
157
152
|
},
|
|
158
153
|
zhipu: {
|
|
159
154
|
displayName: "Zhipu (Z.ai / BigModel)",
|
|
160
155
|
baseUrl: "https://open.bigmodel.cn/api/paas/v4",
|
|
161
156
|
keyless: false,
|
|
162
|
-
|
|
157
|
+
keyHint: "open.bigmodel.cn/usercenter/apikeys",
|
|
158
|
+
},
|
|
159
|
+
zai: {
|
|
160
|
+
displayName: "Z.ai",
|
|
161
|
+
baseUrl: "https://api.z.ai/api/paas/v4",
|
|
162
|
+
keyless: false,
|
|
163
|
+
keyHint: "platform.z.ai",
|
|
163
164
|
},
|
|
164
165
|
cohere: {
|
|
165
166
|
displayName: "Cohere",
|
|
166
167
|
baseUrl: "https://api.cohere.com/compatibility/v1",
|
|
167
168
|
keyless: false,
|
|
169
|
+
keyHint: "dashboard.cohere.com/api-keys",
|
|
170
|
+
},
|
|
171
|
+
deepseek: {
|
|
172
|
+
displayName: "DeepSeek",
|
|
173
|
+
baseUrl: "https://api.deepseek.com/v1",
|
|
174
|
+
keyless: false,
|
|
175
|
+
keyHint: "platform.deepseek.com/api_keys",
|
|
176
|
+
},
|
|
177
|
+
xai: {
|
|
178
|
+
displayName: "xAI (Grok)",
|
|
179
|
+
baseUrl: "https://api.x.ai/v1",
|
|
180
|
+
keyless: false,
|
|
181
|
+
keyHint: "console.x.ai",
|
|
182
|
+
},
|
|
183
|
+
huggingface: {
|
|
184
|
+
displayName: "Hugging Face",
|
|
185
|
+
baseUrl: "https://router.huggingface.co/v1",
|
|
186
|
+
keyless: false,
|
|
187
|
+
keyHint: "huggingface.co/settings/tokens",
|
|
188
|
+
fallbackModels: [
|
|
189
|
+
"meta-llama/Llama-3.3-70B-Instruct",
|
|
190
|
+
"meta-llama/Meta-Llama-3-8B-Instruct",
|
|
191
|
+
"mistralai/Mistral-7B-Instruct-v0.3",
|
|
192
|
+
"Qwen/Qwen2.5-72B-Instruct",
|
|
193
|
+
],
|
|
194
|
+
},
|
|
195
|
+
moonshot: {
|
|
196
|
+
displayName: "Moonshot (Kimi)",
|
|
197
|
+
baseUrl: "https://api.moonshot.ai/v1",
|
|
198
|
+
keyless: false,
|
|
199
|
+
keyHint: "platform.moonshot.ai/console/api-key",
|
|
200
|
+
},
|
|
201
|
+
minimax: {
|
|
202
|
+
displayName: "MiniMax",
|
|
203
|
+
baseUrl: "https://api.minimax.io/v1",
|
|
204
|
+
keyless: false,
|
|
205
|
+
keyHint: "platform.minimax.io",
|
|
206
|
+
},
|
|
207
|
+
venice: {
|
|
208
|
+
displayName: "Venice AI",
|
|
209
|
+
baseUrl: "https://api.venice.ai/api/v1",
|
|
210
|
+
keyless: false,
|
|
211
|
+
keyHint: "venice.ai/settings/api",
|
|
168
212
|
},
|
|
169
213
|
ollama: {
|
|
170
214
|
displayName: "Ollama (local, keyless)",
|
|
@@ -172,6 +216,32 @@ const TEMPLATES: Record<string, {
|
|
|
172
216
|
keyless: true,
|
|
173
217
|
promptUrl: true,
|
|
174
218
|
},
|
|
219
|
+
ollama_cloud: {
|
|
220
|
+
displayName: "Ollama Cloud",
|
|
221
|
+
baseUrl: "https://ollama.com/v1",
|
|
222
|
+
keyless: false,
|
|
223
|
+
keyHint: "ollama.com/settings/api-keys",
|
|
224
|
+
},
|
|
225
|
+
llmproxy: {
|
|
226
|
+
displayName: "llmproxy (local)",
|
|
227
|
+
baseUrl: "http://localhost:8080/v1",
|
|
228
|
+
keyless: true,
|
|
229
|
+
promptUrl: true,
|
|
230
|
+
},
|
|
231
|
+
vercel: {
|
|
232
|
+
displayName: "Vercel AI Gateway",
|
|
233
|
+
baseUrl: "https://ai-gateway.vercel.sh/v1",
|
|
234
|
+
keyless: false,
|
|
235
|
+
keyHint: "vercel.com/account/tokens",
|
|
236
|
+
},
|
|
237
|
+
opencode_zen: {
|
|
238
|
+
displayName: "OpenCode Zen",
|
|
239
|
+
baseUrl: "https://opencode.ai/zen/v1",
|
|
240
|
+
keyless: false,
|
|
241
|
+
keyHint: "opencode.ai",
|
|
242
|
+
// Verify current model IDs via GET /v1/models — the free model list may change.
|
|
243
|
+
modelFilter: ["big-pickle", "deepseek-v4-flash-free", "minimax-m2.5-free", "nemotron-3-super-free"],
|
|
244
|
+
},
|
|
175
245
|
custom: {
|
|
176
246
|
displayName: "Custom Endpoint",
|
|
177
247
|
baseUrl: "",
|
|
@@ -226,6 +296,21 @@ function saveConfig(config: ExtensionConfig): void {
|
|
|
226
296
|
// Networking
|
|
227
297
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
228
298
|
|
|
299
|
+
function isLocalUrl(url: string): boolean {
|
|
300
|
+
try {
|
|
301
|
+
const { hostname } = new URL(url);
|
|
302
|
+
return (
|
|
303
|
+
hostname === "localhost" ||
|
|
304
|
+
hostname === "127.0.0.1" ||
|
|
305
|
+
hostname === "::1" ||
|
|
306
|
+
hostname.endsWith(".local") ||
|
|
307
|
+
hostname.endsWith(".localhost")
|
|
308
|
+
);
|
|
309
|
+
} catch {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
229
314
|
async function fetchModels(baseUrl: string, apiKey: string | null): Promise<CachedModel[]> {
|
|
230
315
|
const url = `${baseUrl.replace(/\/+$/, "")}/models`;
|
|
231
316
|
const headers: Record<string, string> = { Accept: "application/json" };
|
|
@@ -268,9 +353,9 @@ function compatKey(key: string): string {
|
|
|
268
353
|
|
|
269
354
|
function registerProvider(pi: ExtensionAPI, key: string, p: ProviderConfig): void {
|
|
270
355
|
pi.registerProvider(compatKey(key), {
|
|
271
|
-
name:
|
|
356
|
+
name: `compat/${key.replace(/_/g, "-")}`,
|
|
272
357
|
baseUrl: p.baseUrl,
|
|
273
|
-
apiKey: p.apiKey ?? "
|
|
358
|
+
apiKey: p.apiKey ?? (isLocalUrl(p.baseUrl) ? "local" : ""),
|
|
274
359
|
api: "openai-completions" as const,
|
|
275
360
|
models: buildProviderModels(p.cachedModels),
|
|
276
361
|
});
|
|
@@ -359,8 +444,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
359
444
|
|
|
360
445
|
// Step 2 — base URL
|
|
361
446
|
let baseUrl = tpl.baseUrl;
|
|
362
|
-
if (key === "
|
|
363
|
-
// Ask for just the account ID and splice it into the template URL.
|
|
447
|
+
if (key === "cloudflare_workers") {
|
|
364
448
|
const entered = await ctx.ui.input(
|
|
365
449
|
"Account ID",
|
|
366
450
|
"Your Cloudflare Account ID (find it on the Cloudflare dashboard overview page):",
|
|
@@ -370,10 +454,41 @@ export default async function (pi: ExtensionAPI) {
|
|
|
370
454
|
const accountId = entered.trim();
|
|
371
455
|
if (!accountId) { ctx.ui.notify("Account ID cannot be empty.", "error"); return; }
|
|
372
456
|
baseUrl = tpl.baseUrl.replace("YOUR_ACCOUNT_ID", accountId);
|
|
457
|
+
} else if (key === "cloudflare_ai_gateway") {
|
|
458
|
+
const accountIdInput = await ctx.ui.input(
|
|
459
|
+
"Account ID",
|
|
460
|
+
"Your Cloudflare Account ID (find it on the Cloudflare dashboard overview page):",
|
|
461
|
+
""
|
|
462
|
+
);
|
|
463
|
+
if (accountIdInput == null) { ctx.ui.notify("Login cancelled.", "info"); return; }
|
|
464
|
+
const accountId = accountIdInput.trim();
|
|
465
|
+
if (!accountId) { ctx.ui.notify("Account ID cannot be empty.", "error"); return; }
|
|
466
|
+
|
|
467
|
+
const gatewayInput = await ctx.ui.input(
|
|
468
|
+
"Gateway Name",
|
|
469
|
+
"Your AI Gateway name/slug (find it under AI → AI Gateway in the Cloudflare dashboard):",
|
|
470
|
+
""
|
|
471
|
+
);
|
|
472
|
+
if (gatewayInput == null) { ctx.ui.notify("Login cancelled.", "info"); return; }
|
|
473
|
+
const gatewaySlug = gatewayInput.trim();
|
|
474
|
+
if (!gatewaySlug) { ctx.ui.notify("Gateway name cannot be empty.", "error"); return; }
|
|
475
|
+
|
|
476
|
+
const providerInput = await ctx.ui.input(
|
|
477
|
+
"Provider",
|
|
478
|
+
"Upstream provider slug (e.g. openai, workers-ai, anthropic — must match your gateway config):",
|
|
479
|
+
"openai"
|
|
480
|
+
);
|
|
481
|
+
if (providerInput == null) { ctx.ui.notify("Login cancelled.", "info"); return; }
|
|
482
|
+
const provider = providerInput.trim() || "openai";
|
|
483
|
+
|
|
484
|
+
baseUrl = tpl.baseUrl
|
|
485
|
+
.replace("YOUR_ACCOUNT_ID", accountId)
|
|
486
|
+
.replace("YOUR_GATEWAY_SLUG", gatewaySlug)
|
|
487
|
+
.replace("YOUR_PROVIDER", provider);
|
|
373
488
|
} else if (tpl.promptUrl) {
|
|
374
489
|
const defaultUrl = tpl.baseUrl;
|
|
375
|
-
const prompt =
|
|
376
|
-
? `
|
|
490
|
+
const prompt = isLocalUrl(defaultUrl)
|
|
491
|
+
? `Base URL — press Enter for default (${defaultUrl}):`
|
|
377
492
|
: "Base URL of your endpoint (e.g. https://api.example.com/v1):";
|
|
378
493
|
const entered = await ctx.ui.input("Base URL", prompt, defaultUrl);
|
|
379
494
|
if (entered == null) { ctx.ui.notify("Login cancelled.", "info"); return; }
|
|
@@ -381,14 +496,13 @@ export default async function (pi: ExtensionAPI) {
|
|
|
381
496
|
if (!baseUrl) { ctx.ui.notify("Base URL cannot be empty.", "error"); return; }
|
|
382
497
|
}
|
|
383
498
|
|
|
384
|
-
// Step 3 — API key
|
|
499
|
+
// Step 3 — API key (skipped for keyless templates and detected local URLs)
|
|
385
500
|
let apiKey: string | null = null;
|
|
386
|
-
if (!tpl.keyless) {
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
"Your API key — leave blank if keyless:"
|
|
390
|
-
|
|
391
|
-
);
|
|
501
|
+
if (!tpl.keyless && !isLocalUrl(baseUrl)) {
|
|
502
|
+
const keyPrompt = tpl.keyHint
|
|
503
|
+
? `Your API key (required) — get it at ${tpl.keyHint}:`
|
|
504
|
+
: "Your API key — leave blank if keyless:";
|
|
505
|
+
const entered = await ctx.ui.input("API Key", keyPrompt, "");
|
|
392
506
|
if (entered == null) { ctx.ui.notify("Login cancelled.", "info"); return; }
|
|
393
507
|
apiKey = entered.trim() || null;
|
|
394
508
|
}
|
|
@@ -399,8 +513,16 @@ export default async function (pi: ExtensionAPI) {
|
|
|
399
513
|
try {
|
|
400
514
|
models = await fetchModels(baseUrl, apiKey);
|
|
401
515
|
} catch (err) {
|
|
402
|
-
|
|
403
|
-
|
|
516
|
+
if (tpl.fallbackModels && tpl.fallbackModels.length > 0) {
|
|
517
|
+
ctx.ui.notify(
|
|
518
|
+
`Could not fetch model list from ${tpl.displayName} (${err}).\nUsing built-in model list instead.`,
|
|
519
|
+
"warning"
|
|
520
|
+
);
|
|
521
|
+
models = tpl.fallbackModels.map((id) => ({ id }));
|
|
522
|
+
} else {
|
|
523
|
+
ctx.ui.notify(`Connection failed — not saved.\n${err}`, "error");
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
404
526
|
}
|
|
405
527
|
if (!models.length) {
|
|
406
528
|
ctx.ui.notify("Connected but no models returned. Check URL and key.", "error");
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@billjr99/pi-openai-compat",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.9",
|
|
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",
|
|
7
7
|
"homepage": "https://github.com/BillJr99/pi-openai-compat#readme",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "https://github.com/BillJr99/pi-openai-compat.git"
|
|
10
|
+
"url": "git+https://github.com/BillJr99/pi-openai-compat.git"
|
|
11
11
|
},
|
|
12
12
|
"bugs": {
|
|
13
13
|
"url": "https://github.com/BillJr99/pi-openai-compat/issues"
|
|
@@ -17,7 +17,11 @@
|
|
|
17
17
|
"README.md"
|
|
18
18
|
],
|
|
19
19
|
"pi": {
|
|
20
|
-
"extensions": [
|
|
20
|
+
"extensions": [
|
|
21
|
+
"./index.ts"
|
|
22
|
+
]
|
|
21
23
|
},
|
|
22
|
-
"
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"typescript": "^6.0.3"
|
|
26
|
+
}
|
|
23
27
|
}
|