pi-zengate-models 1.0.1
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/CHANGELOG.md +21 -0
- package/LICENSE +21 -0
- package/README.md +112 -0
- package/extensions/zengate-models.ts +281 -0
- package/package.json +31 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [1.0.1] - 2026-11-04
|
|
4
|
+
|
|
5
|
+
- Remove the built-in gateway URL. Every machine sets `OPENCODE2API_BASE_URL`
|
|
6
|
+
or `baseUrl` in `<agent-dir>/zengate.json`.
|
|
7
|
+
- Warn once per session when no endpoint is configured, instead of guessing.
|
|
8
|
+
- Add a test that fails if the extension source gains a URL or a host.
|
|
9
|
+
|
|
10
|
+
## [1.0.0] - 2026-11-04
|
|
11
|
+
|
|
12
|
+
- First release. Reads the ZenGate `/v1/models` catalog at startup and
|
|
13
|
+
registers the free models as Pi provider `opencode2api`.
|
|
14
|
+
- Keeps limits, input modes, zero cost, reasoning flag and thinking level map
|
|
15
|
+
from the gateway metadata.
|
|
16
|
+
- Sends `reasoning_effort` for every registered model, so the level you pick
|
|
17
|
+
reaches the gateway.
|
|
18
|
+
- Caches the catalog in `<agent-dir>/cache/zengate-models.json` and falls back
|
|
19
|
+
to it when the gateway is unreachable.
|
|
20
|
+
- Adds `/zengate` and `/zengate refresh`.
|
|
21
|
+
- Reads settings from the environment or `<agent-dir>/zengate.json`.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hamza (ham-zax)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# pi-zengate-models
|
|
2
|
+
|
|
3
|
+
Pi provider extension for a ZenGate / opencode2api gateway. It reads the
|
|
4
|
+
gateway's `/v1/models` catalog, registers the free models as a Pi provider, and
|
|
5
|
+
keeps the metadata current on every start. No scheduler, no generated
|
|
6
|
+
`models.json`, no Python, and the same file on Linux, macOS and Windows.
|
|
7
|
+
|
|
8
|
+
This package ships no endpoint and no key. Each machine supplies both.
|
|
9
|
+
|
|
10
|
+
## What you get
|
|
11
|
+
|
|
12
|
+
- Model list, context and output limits, input modes, zero cost and reasoning
|
|
13
|
+
levels come from the gateway catalog, which reads models.dev.
|
|
14
|
+
- Only free models reach Pi. The gateway marks paid models excluded, and this
|
|
15
|
+
extension also skips any model whose input, output, cache read or cache write
|
|
16
|
+
cost is not exactly `0`.
|
|
17
|
+
- The thinking level map follows the levels each model declares. Pi shows only
|
|
18
|
+
those levels in `/thinking` and sends only those values.
|
|
19
|
+
- A cached catalog keeps `pi --list-models` honest when the gateway is offline or
|
|
20
|
+
slow. A startup fetch has a 5 second cap.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pi install npm:pi-zengate-models
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
From git, or from a local checkout:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pi install git:github.com/ham-zax/pi-zengate@v1.0.1
|
|
32
|
+
pi install /path/to/pi-zengate
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Configure
|
|
36
|
+
|
|
37
|
+
Every machine needs its own gateway URL and key. Set them in the environment:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
export OPENCODE2API_BASE_URL="https://your-gateway.example/v1"
|
|
41
|
+
export OPENCODE2API_API_KEY="..." # Linux, macOS, WSL
|
|
42
|
+
setx OPENCODE2API_BASE_URL "https://your-gateway.example/v1" # Windows
|
|
43
|
+
setx OPENCODE2API_API_KEY "..." # Windows
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
or in `<agent-dir>/zengate.json`, which avoids environment variables:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"baseUrl": "https://your-gateway.example/v1",
|
|
51
|
+
"apiKey": "..."
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`<agent-dir>` is `~/.pi/agent` by default, or `PI_CODING_AGENT_DIR` when set.
|
|
56
|
+
The environment wins over the file. Without a URL the extension registers no
|
|
57
|
+
provider and shows one warning per session.
|
|
58
|
+
|
|
59
|
+
## Verify
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pi --list-models # opencode2api rows with limits and thinking=yes
|
|
63
|
+
pi
|
|
64
|
+
/zengate # count, base URL, key source, last catalog check
|
|
65
|
+
/zengate refresh # fetch the catalog now
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Then set the default model in `<agent-dir>/settings.json`:
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"defaultProvider": "opencode2api",
|
|
73
|
+
"defaultModel": "deepseek-v4.1-flash-free"
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Remove the old generated provider
|
|
78
|
+
|
|
79
|
+
If `models.json` also defines `providers.opencode2api`, those static entries win
|
|
80
|
+
over the extension by model ID, so stale limits and a stale compat block return.
|
|
81
|
+
Delete that provider block, or leave `models.json` as:
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{ "providers": {} }
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Choosing a thinking level
|
|
88
|
+
|
|
89
|
+
Run `/thinking`, or pass `--thinking <level>` on the command line. Pi clamps a
|
|
90
|
+
level the model does not declare, which means `max` on a model without `max`
|
|
91
|
+
sends the highest level that model does declare. A model that declares no levels
|
|
92
|
+
sends nothing beyond `high`.
|
|
93
|
+
|
|
94
|
+
The compat flag `supportsReasoningEffort` is `true` for every model this provider
|
|
95
|
+
registers. When it is `false`, Pi drops the selected level before the request and
|
|
96
|
+
the model always runs at its own default.
|
|
97
|
+
|
|
98
|
+
## Files it writes
|
|
99
|
+
|
|
100
|
+
- Cache: `<agent-dir>/cache/zengate-models.json`
|
|
101
|
+
- Nothing else. The extension never edits `models.json` or `settings.json`.
|
|
102
|
+
|
|
103
|
+
## Develop
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
npm test # mapping and settings tests, no network
|
|
107
|
+
pi -e ./extensions/zengate-models.ts # load from source
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
|
|
112
|
+
MIT
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ZenGate (opencode2api) model discovery for Pi.
|
|
3
|
+
*
|
|
4
|
+
* The gateway's /v1/models catalog is the authority for which free models
|
|
5
|
+
* exist and what they accept, cost and support. This extension registers the
|
|
6
|
+
* provider, reads that catalog at startup, and caches it, so every OS gets
|
|
7
|
+
* current models and metadata with no scheduled job and no generated
|
|
8
|
+
* models.json.
|
|
9
|
+
*
|
|
10
|
+
* The mapping mirrors scripts/sync-client-models.py in the gateway repository:
|
|
11
|
+
* `metadata` carries the models.dev values, non-free or non-text models are
|
|
12
|
+
* skipped, and Pi keeps only text and image inputs.
|
|
13
|
+
*/
|
|
14
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { homedir } from "node:os";
|
|
17
|
+
import { dirname, join } from "node:path";
|
|
18
|
+
|
|
19
|
+
export const PROVIDER_ID = "opencode2api";
|
|
20
|
+
export const PROVIDER_NAME = "ZenGate";
|
|
21
|
+
export const KEY_ENV = "OPENCODE2API_API_KEY";
|
|
22
|
+
export const BASE_URL_ENV = "OPENCODE2API_BASE_URL";
|
|
23
|
+
|
|
24
|
+
const FETCH_TIMEOUT_MS = 5000;
|
|
25
|
+
const LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
26
|
+
|
|
27
|
+
type Inputs = ("text" | "image")[];
|
|
28
|
+
export type PiModel = {
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
api: "openai-completions" | "openai-responses";
|
|
32
|
+
reasoning: boolean;
|
|
33
|
+
input: Inputs;
|
|
34
|
+
contextWindow: number;
|
|
35
|
+
maxTokens: number;
|
|
36
|
+
cost: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
|
37
|
+
thinkingLevelMap?: Record<string, string | null>;
|
|
38
|
+
compat: {
|
|
39
|
+
supportsStore: boolean;
|
|
40
|
+
supportsDeveloperRole: boolean;
|
|
41
|
+
supportsReasoningEffort: boolean;
|
|
42
|
+
supportsUsageInStreaming: boolean;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type Meta = {
|
|
47
|
+
name?: unknown;
|
|
48
|
+
limit?: { context?: unknown; output?: unknown };
|
|
49
|
+
modalities?: { input?: unknown; output?: unknown };
|
|
50
|
+
reasoning?: unknown;
|
|
51
|
+
cost?: { input?: unknown; output?: unknown; cache_read?: unknown; cache_write?: unknown };
|
|
52
|
+
reasoning_options?: unknown;
|
|
53
|
+
};
|
|
54
|
+
type CatalogRecord = { id?: unknown; endpoint?: unknown; metadata?: Meta };
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Pi sends reasoning_effort only when this is true. Every model in the current
|
|
58
|
+
* free catalog accepts the field, and the thinking level map decides which
|
|
59
|
+
* values Pi may send.
|
|
60
|
+
*/
|
|
61
|
+
const COMPAT = {
|
|
62
|
+
supportsStore: false,
|
|
63
|
+
supportsDeveloperRole: false,
|
|
64
|
+
supportsReasoningEffort: true,
|
|
65
|
+
supportsUsageInStreaming: true,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const isText = (value: unknown): value is string => typeof value === "string" && value.length > 0;
|
|
69
|
+
const isCount = (value: unknown): value is number => Number.isInteger(value) && (value as number) > 0;
|
|
70
|
+
const isFree = (value: unknown): value is number => typeof value === "number" && value === 0;
|
|
71
|
+
|
|
72
|
+
/** Upstream effort levels for one model; empty when it has no graded control. */
|
|
73
|
+
export function effortValues(meta: Meta): string[] {
|
|
74
|
+
if (!Array.isArray(meta.reasoning_options)) return [];
|
|
75
|
+
for (const option of meta.reasoning_options) {
|
|
76
|
+
if (!option || typeof option !== "object") continue;
|
|
77
|
+
const { type, values } = option as { type?: unknown; values?: unknown };
|
|
78
|
+
if (type === "effort" && Array.isArray(values) && values.length > 0 && values.every(isText)) {
|
|
79
|
+
return [...values];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* A supported level sends its own name. A level the model does not declare maps
|
|
87
|
+
* to null, so Pi hides it and never sends it.
|
|
88
|
+
*/
|
|
89
|
+
export function thinkingLevelMap(values: string[]): Record<string, string | null> {
|
|
90
|
+
const map: Record<string, string | null> = {};
|
|
91
|
+
for (const level of LEVELS) {
|
|
92
|
+
if (level === "off") map[level] = values.includes("none") ? "none" : null;
|
|
93
|
+
else map[level] = values.includes(level) ? level : null;
|
|
94
|
+
}
|
|
95
|
+
return map;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Map one gateway catalog payload to Pi model configs. */
|
|
99
|
+
export function toPiModels(payload: unknown): PiModel[] {
|
|
100
|
+
const data = (payload as { data?: unknown } | null)?.data;
|
|
101
|
+
if (!Array.isArray(data)) throw new Error("gateway catalog has no data array");
|
|
102
|
+
|
|
103
|
+
const models: PiModel[] = [];
|
|
104
|
+
for (const record of data as CatalogRecord[]) {
|
|
105
|
+
const meta = record?.metadata;
|
|
106
|
+
if (!meta || !isText(record.id) || !isText(meta.name)) continue;
|
|
107
|
+
const contextWindow = meta.limit?.context;
|
|
108
|
+
const maxTokens = meta.limit?.output;
|
|
109
|
+
if (!isCount(contextWindow) || !isCount(maxTokens)) continue;
|
|
110
|
+
const outputs = meta.modalities?.output;
|
|
111
|
+
if (!Array.isArray(outputs) || !outputs.includes("text")) continue;
|
|
112
|
+
const cost = meta.cost;
|
|
113
|
+
if (!cost || !isFree(cost.input) || !isFree(cost.output)) continue;
|
|
114
|
+
if (!isFree(cost.cache_read ?? 0) || !isFree(cost.cache_write ?? 0)) continue;
|
|
115
|
+
const declared = Array.isArray(meta.modalities?.input) ? meta.modalities.input : [];
|
|
116
|
+
const input = ["text", "image"].filter((mode) => declared.includes(mode)) as Inputs;
|
|
117
|
+
if (input.length === 0) continue;
|
|
118
|
+
const efforts = effortValues(meta);
|
|
119
|
+
models.push({
|
|
120
|
+
id: record.id,
|
|
121
|
+
name: meta.name,
|
|
122
|
+
api: record.endpoint === "responses" ? "openai-responses" : "openai-completions",
|
|
123
|
+
reasoning: meta.reasoning === true,
|
|
124
|
+
input,
|
|
125
|
+
contextWindow,
|
|
126
|
+
maxTokens,
|
|
127
|
+
cost: {
|
|
128
|
+
input: cost.input,
|
|
129
|
+
output: cost.output,
|
|
130
|
+
cacheRead: cost.cache_read ?? 0,
|
|
131
|
+
cacheWrite: cost.cache_write ?? 0,
|
|
132
|
+
},
|
|
133
|
+
...(efforts.length > 0 ? { thinkingLevelMap: thinkingLevelMap(efforts) } : {}),
|
|
134
|
+
compat: COMPAT,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
if (models.length === 0) throw new Error("gateway catalog has no usable free models");
|
|
138
|
+
return models;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Pi config directory, honouring PI_CODING_AGENT_DIR. */
|
|
142
|
+
export function agentDir(): string {
|
|
143
|
+
return process.env.PI_CODING_AGENT_DIR || join(homedir(), ".pi", "agent");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function configPath(): string {
|
|
147
|
+
return join(agentDir(), "zengate.json");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function cachePath(): string {
|
|
151
|
+
return join(agentDir(), "cache", "zengate-models.json");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function readJson(path: string): Record<string, unknown> | undefined {
|
|
155
|
+
try {
|
|
156
|
+
const value = JSON.parse(readFileSync(path, "utf8")) as unknown;
|
|
157
|
+
return value && typeof value === "object" ? (value as Record<string, unknown>) : undefined;
|
|
158
|
+
} catch {
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function normalizeBaseUrl(value: string): string {
|
|
164
|
+
return value.trim().replace(/\/+$/, "");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Settings precedence: environment, then <agent-dir>/zengate.json.
|
|
169
|
+
* The config file lets a machine skip environment variables. This package ships
|
|
170
|
+
* no endpoint: every machine supplies its own.
|
|
171
|
+
*/
|
|
172
|
+
export function readSettings(): { baseUrl?: string; apiKey?: string; keySource: string } {
|
|
173
|
+
const file = readJson(configPath()) ?? {};
|
|
174
|
+
const fileBase = typeof file.baseUrl === "string" && file.baseUrl.trim() ? file.baseUrl : undefined;
|
|
175
|
+
const fileKey = typeof file.apiKey === "string" && file.apiKey.trim() ? file.apiKey.trim() : undefined;
|
|
176
|
+
const envBase = process.env[BASE_URL_ENV];
|
|
177
|
+
const envKey = process.env[KEY_ENV]?.trim();
|
|
178
|
+
const base = envBase || fileBase;
|
|
179
|
+
return {
|
|
180
|
+
baseUrl: base ? normalizeBaseUrl(base) : undefined,
|
|
181
|
+
apiKey: envKey || fileKey,
|
|
182
|
+
keySource: envKey ? KEY_ENV : fileKey ? "zengate.json" : "none",
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const configureHint = (): string => `Set ${BASE_URL_ENV} or baseUrl in ${configPath()} to your gateway /v1 URL.`;
|
|
187
|
+
|
|
188
|
+
export default async function (pi: ExtensionAPI) {
|
|
189
|
+
const settings = readSettings();
|
|
190
|
+
const key = settings.apiKey;
|
|
191
|
+
const cacheFile = cachePath();
|
|
192
|
+
|
|
193
|
+
// Without an endpoint there is nothing to discover. Say so once per session
|
|
194
|
+
// and leave the model list untouched.
|
|
195
|
+
if (!settings.baseUrl) {
|
|
196
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
197
|
+
ctx.ui.notify(`${PROVIDER_NAME}: ${configureHint()}`, "warning");
|
|
198
|
+
});
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const baseUrl = settings.baseUrl;
|
|
202
|
+
|
|
203
|
+
const readCache = (): PiModel[] => {
|
|
204
|
+
const saved = readJson(cacheFile);
|
|
205
|
+
return Array.isArray(saved?.models) ? (saved.models as PiModel[]) : [];
|
|
206
|
+
};
|
|
207
|
+
const writeCache = (models: PiModel[]) => {
|
|
208
|
+
try {
|
|
209
|
+
mkdirSync(dirname(cacheFile), { recursive: true });
|
|
210
|
+
writeFileSync(
|
|
211
|
+
cacheFile,
|
|
212
|
+
`${JSON.stringify({ baseUrl, checkedAt: Date.now(), models }, null, 2)}\n`,
|
|
213
|
+
);
|
|
214
|
+
} catch {
|
|
215
|
+
// A read-only agent directory must not break model loading.
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
let latest = readCache();
|
|
220
|
+
let checkedAt = readJson(cacheFile)?.checkedAt as number | undefined;
|
|
221
|
+
|
|
222
|
+
const refresh = async (signal?: AbortSignal): Promise<PiModel[]> => {
|
|
223
|
+
const deadline = AbortSignal.timeout(FETCH_TIMEOUT_MS);
|
|
224
|
+
const combined = signal && "any" in AbortSignal ? AbortSignal.any([signal, deadline]) : deadline;
|
|
225
|
+
const response = await fetch(`${baseUrl}/models`, {
|
|
226
|
+
headers: { accept: "application/json", ...(key ? { authorization: `Bearer ${key}` } : {}) },
|
|
227
|
+
signal: combined,
|
|
228
|
+
});
|
|
229
|
+
if (!response.ok) throw new Error(`gateway catalog returned HTTP ${response.status}`);
|
|
230
|
+
latest = toPiModels(await response.json());
|
|
231
|
+
checkedAt = Date.now();
|
|
232
|
+
writeCache(latest);
|
|
233
|
+
return latest;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// Fetch before registering, so `pi --list-models` and every startup see the
|
|
237
|
+
// current catalog. `--list-models` runs offline-only, so it never refreshes.
|
|
238
|
+
if (key) {
|
|
239
|
+
try {
|
|
240
|
+
await refresh();
|
|
241
|
+
} catch {
|
|
242
|
+
// Offline or a gateway fault: the cached catalog above is still usable.
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
pi.registerProvider(PROVIDER_ID, {
|
|
247
|
+
name: PROVIDER_NAME,
|
|
248
|
+
baseUrl,
|
|
249
|
+
apiKey: key ?? `\${${KEY_ENV}}`,
|
|
250
|
+
api: "openai-completions",
|
|
251
|
+
models: latest,
|
|
252
|
+
async refreshModels(context) {
|
|
253
|
+
if (!context.allowNetwork) return latest;
|
|
254
|
+
const credential = context.credential?.type === "api_key" ? context.credential.key : undefined;
|
|
255
|
+
if (!credential && !key) return latest;
|
|
256
|
+
try {
|
|
257
|
+
return await refresh(context.signal);
|
|
258
|
+
} catch {
|
|
259
|
+
return latest;
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
pi.registerCommand("zengate", {
|
|
265
|
+
description: "Show or refresh the ZenGate model catalog",
|
|
266
|
+
handler: async (args, ctx) => {
|
|
267
|
+
if ((args ?? "").trim() === "refresh") {
|
|
268
|
+
try {
|
|
269
|
+
const models = await refresh();
|
|
270
|
+
ctx.ui.notify(`ZenGate: refreshed ${models.length} models from ${baseUrl}`, "info");
|
|
271
|
+
} catch (error) {
|
|
272
|
+
ctx.ui.notify(`ZenGate refresh failed: ${(error as Error).message}`, "error");
|
|
273
|
+
}
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const stamp = checkedAt ? new Date(checkedAt).toISOString() : "never";
|
|
277
|
+
const line = `${PROVIDER_NAME}: ${latest.length} models, ${baseUrl}, key ${settings.keySource}, catalog ${stamp}`;
|
|
278
|
+
ctx.ui.notify(line, "info");
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-zengate-models",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Pi provider extension that discovers free models and their metadata from an OpenAI-compatible gateway catalog, with a local cache and no scheduler.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"pi-extension",
|
|
8
|
+
"opencode2api",
|
|
9
|
+
"zengate",
|
|
10
|
+
"openai-compatible"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=20.19"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"extensions",
|
|
19
|
+
"README.md",
|
|
20
|
+
"CHANGELOG.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
23
|
+
"pi": {
|
|
24
|
+
"extensions": [
|
|
25
|
+
"./extensions/zengate-models.ts"
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"test": "node --test test/mapping.test.mjs"
|
|
30
|
+
}
|
|
31
|
+
}
|