@billjr99/pi-openai-compat 1.1.22 → 1.1.23

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 +36 -1
  2. package/index.ts +48 -5
  3. 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?: string[];
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 string[]),
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
  }
@@ -896,7 +939,7 @@ export default async function (pi: ExtensionAPI) {
896
939
  if (models.length > 0) {
897
940
  // Only overwrite the cache on a successful, non-empty fetch — a
898
941
  // flaky refresh must never blank out a working provider's models.
899
- p.cachedModels = models;
942
+ p.cachedModels = mergeModelMetadata(p.cachedModels, models);
900
943
  saveConfig(config);
901
944
  registerProvider(pi, key, p);
902
945
  refreshed.push(`${p.displayName} (${models.length})`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@billjr99/pi-openai-compat",
3
- "version": "1.1.22",
3
+ "version": "1.1.23",
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",