@billjr99/pi-openai-compat 1.1.22 → 1.1.24
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 +36 -1
- package/index.ts +56 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -129,7 +129,10 @@ dropped) since you logged in, without restarting your session.
|
|
|
129
129
|
If you have multiple providers registered, you are asked which one to refresh
|
|
130
130
|
(or choose **All providers**); with a single provider it refreshes directly.
|
|
131
131
|
A failed or empty refresh leaves the existing model list untouched, so a flaky
|
|
132
|
-
network call can't blank out a working provider.
|
|
132
|
+
network call can't blank out a working provider. Capability metadata you have
|
|
133
|
+
hand-edited into `cachedModels` (see [Config file](#config-file)) survives a
|
|
134
|
+
refresh: the fetched list decides which model IDs exist, and any field the
|
|
135
|
+
provider's catalog omits is carried forward from the previous cache.
|
|
133
136
|
|
|
134
137
|
### `/compat-logout`
|
|
135
138
|
|
|
@@ -199,6 +202,38 @@ Credentials and cached model lists are stored at:
|
|
|
199
202
|
API keys are stored in plaintext. Protect the file with `chmod 600` if
|
|
200
203
|
needed, or delete it to clear all saved credentials.
|
|
201
204
|
|
|
205
|
+
Each provider's `cachedModels` array holds one entry per model. Only `id` is
|
|
206
|
+
required; the rest are optional and fall back to conservative defaults when the
|
|
207
|
+
provider's `/models` catalog does not report them:
|
|
208
|
+
|
|
209
|
+
| Field | Type | Default | Notes |
|
|
210
|
+
|---|---|---|---|
|
|
211
|
+
| `id` | string | required | Model ID as sent in requests. |
|
|
212
|
+
| `contextWindow` | number | `128000` | Context window in tokens. |
|
|
213
|
+
| `maxTokens` | number | `4096` | Maximum output tokens per response. |
|
|
214
|
+
| `reasoning` | boolean | `false` | Enables pi's thinking mode for the model. |
|
|
215
|
+
| `input` | `["text"]` or `["text","image"]` | `["text"]` | Modalities pi may send; `image` lets pi attach image blocks. Any other value is ignored. |
|
|
216
|
+
|
|
217
|
+
Most catalogs report none of these beyond the ID, so aggregators and proxies
|
|
218
|
+
(CLIProxyAPI, for example) register every model as a 128K, text-only,
|
|
219
|
+
non-reasoning model. To correct that, edit the entry by hand and `/reload`:
|
|
220
|
+
|
|
221
|
+
```json
|
|
222
|
+
{
|
|
223
|
+
"providers": {
|
|
224
|
+
"cpa": {
|
|
225
|
+
"cachedModels": [
|
|
226
|
+
{ "id": "claude-sonnet-4-6", "contextWindow": 1000000, "maxTokens": 128000,
|
|
227
|
+
"reasoning": true, "input": ["text", "image"] }
|
|
228
|
+
]
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
`/compat-refresh` keeps these hand-edited fields for any model ID that is still
|
|
235
|
+
present in the refreshed catalog; a field the provider does report always wins.
|
|
236
|
+
|
|
202
237
|
### Adding a provider without pi — `add-provider.sh`
|
|
203
238
|
|
|
204
239
|
`/compat-login` is the normal path, but the repo also ships a standalone script
|
package/index.ts
CHANGED
|
@@ -25,12 +25,18 @@ import * as os from "node:os";
|
|
|
25
25
|
// Types
|
|
26
26
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
27
27
|
|
|
28
|
+
/** Input modalities pi understands; anything else is dropped at fetch time. */
|
|
29
|
+
type ModelInput = "text" | "image";
|
|
30
|
+
|
|
28
31
|
interface CachedModel {
|
|
29
32
|
id: string;
|
|
30
33
|
contextWindow?: number;
|
|
31
34
|
maxTokens?: number;
|
|
35
|
+
// Optional capability metadata. Rarely present in a /models payload, but a
|
|
36
|
+
// user may hand-edit them into cachedModels for providers whose catalog
|
|
37
|
+
// omits them (see README "Config file"); /compat-refresh preserves them.
|
|
32
38
|
reasoning?: boolean;
|
|
33
|
-
input?:
|
|
39
|
+
input?: ModelInput[];
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
interface ProviderConfig {
|
|
@@ -69,9 +75,25 @@ type RawModel = {
|
|
|
69
75
|
name?: string;
|
|
70
76
|
context_window?: number;
|
|
71
77
|
max_tokens?: number;
|
|
78
|
+
reasoning?: unknown;
|
|
79
|
+
input?: unknown;
|
|
72
80
|
task?: { name?: string };
|
|
73
81
|
};
|
|
74
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Coerce an upstream `input` value into pi's modality list. Unknown or
|
|
85
|
+
* malformed values yield undefined so buildProviderModels falls back to the
|
|
86
|
+
* default, rather than caching something pi would choke on (it calls
|
|
87
|
+
* `model.input.includes("image")` at tool time).
|
|
88
|
+
*/
|
|
89
|
+
function normalizeInput(value: unknown): ModelInput[] | undefined {
|
|
90
|
+
if (!Array.isArray(value)) return undefined;
|
|
91
|
+
const kept = value.filter(
|
|
92
|
+
(v): v is ModelInput => v === "text" || v === "image",
|
|
93
|
+
);
|
|
94
|
+
return kept.length > 0 ? kept : undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
75
97
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
76
98
|
// Provider templates
|
|
77
99
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -557,8 +579,8 @@ async function fetchModels(
|
|
|
557
579
|
id,
|
|
558
580
|
contextWindow: m.context_window,
|
|
559
581
|
maxTokens: m.max_tokens,
|
|
560
|
-
reasoning: m.reasoning,
|
|
561
|
-
input: m.input,
|
|
582
|
+
reasoning: typeof m.reasoning === "boolean" ? m.reasoning : undefined,
|
|
583
|
+
input: normalizeInput(m.input),
|
|
562
584
|
};
|
|
563
585
|
})
|
|
564
586
|
.filter((m) => Boolean(m.id))
|
|
@@ -576,7 +598,7 @@ function buildProviderModels(models: CachedModel[]) {
|
|
|
576
598
|
id,
|
|
577
599
|
name: id,
|
|
578
600
|
reasoning: m.reasoning ?? false,
|
|
579
|
-
input: m.input ?? (["text"] as
|
|
601
|
+
input: m.input ?? (["text"] as ModelInput[]),
|
|
580
602
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
581
603
|
contextWindow: m.contextWindow ?? 128_000,
|
|
582
604
|
maxTokens: m.maxTokens ?? 4_096,
|
|
@@ -584,6 +606,27 @@ function buildProviderModels(models: CachedModel[]) {
|
|
|
584
606
|
});
|
|
585
607
|
}
|
|
586
608
|
|
|
609
|
+
/**
|
|
610
|
+
* Carry hand-edited capability metadata (reasoning / input / contextWindow /
|
|
611
|
+
* maxTokens) forward from the previous cache when a fresh /models fetch omits
|
|
612
|
+
* it. The fetched list decides which ids exist; the old cache only fills gaps,
|
|
613
|
+
* so a provider that does report a field always wins.
|
|
614
|
+
*/
|
|
615
|
+
function mergeModelMetadata(previous: CachedModel[], fetched: CachedModel[]): CachedModel[] {
|
|
616
|
+
const prior = new Map(previous.map((m) => [m.id, m]));
|
|
617
|
+
return fetched.map((m) => {
|
|
618
|
+
const old = prior.get(m.id);
|
|
619
|
+
if (!old) return m;
|
|
620
|
+
return {
|
|
621
|
+
...m,
|
|
622
|
+
contextWindow: m.contextWindow ?? old.contextWindow,
|
|
623
|
+
maxTokens: m.maxTokens ?? old.maxTokens,
|
|
624
|
+
reasoning: m.reasoning ?? old.reasoning,
|
|
625
|
+
input: m.input ?? old.input,
|
|
626
|
+
};
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
|
|
587
630
|
function compatKey(key: string): string {
|
|
588
631
|
return `compat-${key}`;
|
|
589
632
|
}
|
|
@@ -718,8 +761,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
718
761
|
if (key === "cloudflare_workers") {
|
|
719
762
|
const entered = await ctx.ui.input(
|
|
720
763
|
"Account ID",
|
|
721
|
-
"Your Cloudflare Account ID (find it on the Cloudflare dashboard overview page):"
|
|
722
|
-
""
|
|
764
|
+
"Your Cloudflare Account ID (find it on the Cloudflare dashboard overview page):"
|
|
723
765
|
);
|
|
724
766
|
if (entered == null) { ctx.ui.notify("Login cancelled.", "info"); return; }
|
|
725
767
|
const accountId = entered.trim();
|
|
@@ -729,8 +771,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
729
771
|
} else if (key === "cloudflare_ai_gateway") {
|
|
730
772
|
const accountIdInput = await ctx.ui.input(
|
|
731
773
|
"Account ID",
|
|
732
|
-
"Your Cloudflare Account ID (find it on the Cloudflare dashboard overview page):"
|
|
733
|
-
""
|
|
774
|
+
"Your Cloudflare Account ID (find it on the Cloudflare dashboard overview page):"
|
|
734
775
|
);
|
|
735
776
|
if (accountIdInput == null) { ctx.ui.notify("Login cancelled.", "info"); return; }
|
|
736
777
|
const accountId = accountIdInput.trim();
|
|
@@ -738,8 +779,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
738
779
|
|
|
739
780
|
const gatewayInput = await ctx.ui.input(
|
|
740
781
|
"Gateway Name",
|
|
741
|
-
"Your AI Gateway name/slug (find it under AI → AI Gateway in the Cloudflare dashboard):"
|
|
742
|
-
""
|
|
782
|
+
"Your AI Gateway name/slug (find it under AI → AI Gateway in the Cloudflare dashboard):"
|
|
743
783
|
);
|
|
744
784
|
if (gatewayInput == null) { ctx.ui.notify("Login cancelled.", "info"); return; }
|
|
745
785
|
const gatewaySlug = gatewayInput.trim();
|
|
@@ -747,8 +787,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
747
787
|
|
|
748
788
|
const providerInput = await ctx.ui.input(
|
|
749
789
|
"Provider",
|
|
750
|
-
"Upstream provider slug (e.g. openai, workers-ai, anthropic — must match your gateway config):"
|
|
751
|
-
"openai"
|
|
790
|
+
"Upstream provider slug (e.g. openai, workers-ai, anthropic — must match your gateway config):"
|
|
752
791
|
);
|
|
753
792
|
if (providerInput == null) { ctx.ui.notify("Login cancelled.", "info"); return; }
|
|
754
793
|
const provider = providerInput.trim() || "openai";
|
|
@@ -768,7 +807,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
768
807
|
const prompt = isLocalUrl(defaultUrl)
|
|
769
808
|
? `Base URL — press Enter for default (${defaultUrl}):`
|
|
770
809
|
: "Base URL of your endpoint (e.g. https://api.example.com/v1):";
|
|
771
|
-
const entered = await ctx.ui.input("Base URL", prompt
|
|
810
|
+
const entered = await ctx.ui.input("Base URL", prompt);
|
|
772
811
|
if (entered == null) { ctx.ui.notify("Login cancelled.", "info"); return; }
|
|
773
812
|
baseUrl = (entered.trim() || defaultUrl).replace(/\/+$/, "");
|
|
774
813
|
if (!baseUrl) { ctx.ui.notify("Base URL cannot be empty.", "error"); return; }
|
|
@@ -780,7 +819,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
780
819
|
const keyPrompt = tpl.keyHint
|
|
781
820
|
? `Your API key (required) — get it at ${tpl.keyHint}:`
|
|
782
821
|
: "Your API key — leave blank if keyless:";
|
|
783
|
-
const entered = await ctx.ui.input("API Key", keyPrompt
|
|
822
|
+
const entered = await ctx.ui.input("API Key", keyPrompt);
|
|
784
823
|
if (entered == null) { ctx.ui.notify("Login cancelled.", "info"); return; }
|
|
785
824
|
apiKey = entered.trim() || null;
|
|
786
825
|
}
|
|
@@ -843,7 +882,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
843
882
|
|
|
844
883
|
ctx.ui.notify(
|
|
845
884
|
`${tpl.displayName} registered — ${models.length} model(s) added to /model.`,
|
|
846
|
-
"
|
|
885
|
+
"info"
|
|
847
886
|
);
|
|
848
887
|
},
|
|
849
888
|
});
|
|
@@ -896,7 +935,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
896
935
|
if (models.length > 0) {
|
|
897
936
|
// Only overwrite the cache on a successful, non-empty fetch — a
|
|
898
937
|
// flaky refresh must never blank out a working provider's models.
|
|
899
|
-
p.cachedModels = models;
|
|
938
|
+
p.cachedModels = mergeModelMetadata(p.cachedModels, models);
|
|
900
939
|
saveConfig(config);
|
|
901
940
|
registerProvider(pi, key, p);
|
|
902
941
|
refreshed.push(`${p.displayName} (${models.length})`);
|
|
@@ -909,7 +948,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
909
948
|
}
|
|
910
949
|
|
|
911
950
|
if (refreshed.length > 0) {
|
|
912
|
-
ctx.ui.notify(`Refreshed: ${refreshed.join(", ")}.`, "
|
|
951
|
+
ctx.ui.notify(`Refreshed: ${refreshed.join(", ")}.`, "info");
|
|
913
952
|
}
|
|
914
953
|
if (failed.length > 0) {
|
|
915
954
|
ctx.ui.notify(
|
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.24",
|
|
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",
|