@mcowger/opencode-plexus 1.3.8 → 1.3.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 +42 -0
- package/dist/index.js +105 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -244,6 +244,47 @@ Both adapters also accept pi-style environment interpolation in configured strin
|
|
|
244
244
|
|
|
245
245
|
---
|
|
246
246
|
|
|
247
|
+
## Model suppression
|
|
248
|
+
|
|
249
|
+
All plugin packages support suppressing models by name or pattern so undesired models do not appear in model selectors or cache restored lists.
|
|
250
|
+
|
|
251
|
+
### Pattern matching syntax
|
|
252
|
+
|
|
253
|
+
- **Exact match** (case-insensitive): Matches model `id`, `name`, or short ID (suffix after `/` or `:`). Example: `"gpt-4o"`, `"Claude 3.5 Sonnet"`.
|
|
254
|
+
- **Glob wildcards**: Supports `*` (0+ characters) and `?` (1 character). Example: `"gpt-3.5*"`, `"*deprecated*"`, `"anthropic/*"`.
|
|
255
|
+
- **Regex patterns**: Prefix with `regex:`. Example: `"regex:^gpt-[34]"`.
|
|
256
|
+
|
|
257
|
+
### Configuration methods
|
|
258
|
+
|
|
259
|
+
- **Environment variables**:
|
|
260
|
+
```sh
|
|
261
|
+
export PLEXUS_SUPPRESS_MODELS="gpt-3.5*, *deprecated*, whisper"
|
|
262
|
+
```
|
|
263
|
+
`PLEXUS_EXCLUDE_MODELS` is also accepted. Values can be comma-, semicolon-, or newline-separated.
|
|
264
|
+
|
|
265
|
+
- **pi / Oh My Pi config**: Add `suppressModels` (or `suppress`) to `config.json`:
|
|
266
|
+
```json
|
|
267
|
+
{
|
|
268
|
+
"baseUrl": "https://plexus.example.com",
|
|
269
|
+
"suppressModels": ["gpt-3.5*", "*deprecated*"]
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
- **OpenCode config**: Add `suppressModels` (or `suppress`) under `provider.plexus.options` in `opencode.json`:
|
|
274
|
+
```json
|
|
275
|
+
{
|
|
276
|
+
"provider": {
|
|
277
|
+
"plexus": {
|
|
278
|
+
"options": {
|
|
279
|
+
"suppressModels": ["gpt-3.5*", "claude-2*"]
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
247
288
|
## Package layout
|
|
248
289
|
|
|
249
290
|
```
|
|
@@ -252,6 +293,7 @@ packages/
|
|
|
252
293
|
src/
|
|
253
294
|
types.ts # wire types (PlexusApiModel, PlexusModelDescriptor, etc.)
|
|
254
295
|
convert.ts # model fetching, conversion, compat detection
|
|
296
|
+
suppress.ts # model pattern suppression / exclusion matching
|
|
255
297
|
index.ts # barrel export
|
|
256
298
|
plexus-pi/ # pi host adapter
|
|
257
299
|
src/
|
package/dist/index.js
CHANGED
|
@@ -9,12 +9,66 @@ var PLEXUS_BASE_URL_OPTION = "plexusBaseURL";
|
|
|
9
9
|
var ENV_BASE_URL = "PLEXUS_BASE_URL";
|
|
10
10
|
var ENV_API_URL = "PLEXUS_API_URL";
|
|
11
11
|
var ENV_API_KEY = "PLEXUS_API_KEY";
|
|
12
|
+
var ENV_SUPPRESS_MODELS = "PLEXUS_SUPPRESS_MODELS";
|
|
13
|
+
var ENV_SUPPRESSED_MODELS = "PLEXUS_SUPPRESSED_MODELS";
|
|
14
|
+
var ENV_EXCLUDE_MODELS = "PLEXUS_EXCLUDE_MODELS";
|
|
15
|
+
var ENV_IGNORE_MODELS = "PLEXUS_IGNORE_MODELS";
|
|
16
|
+
var PLEXUS_SUPPRESS_MODELS_OPTION = "suppressModels";
|
|
12
17
|
var MODELS_FETCH_TIMEOUT_MS = 1e4;
|
|
13
18
|
var REFRESH_TTL_MS = 60000;
|
|
14
19
|
var CONFIG_HOOK_REFRESH_BUDGET_MS = 3000;
|
|
15
20
|
var PLACEHOLDER_MODEL_ID = "plexus-unconfigured";
|
|
16
21
|
var PLEXUS_REFRESH_COMMAND = "plexus-refresh";
|
|
17
22
|
|
|
23
|
+
// ../plexus-models/src/suppress.ts
|
|
24
|
+
function parseSuppressionPatterns(raw) {
|
|
25
|
+
if (!raw)
|
|
26
|
+
return [];
|
|
27
|
+
const items = Array.isArray(raw) ? raw : raw.split(/[\n,;]+/);
|
|
28
|
+
return items.map((s) => s.trim()).filter((s) => s.length > 0);
|
|
29
|
+
}
|
|
30
|
+
function getEnvSuppressedModels() {
|
|
31
|
+
const env = typeof process !== "undefined" && process?.env ? process.env : {};
|
|
32
|
+
const raw = env.PLEXUS_SUPPRESS_MODELS ?? env.PLEXUS_EXCLUDE_MODELS;
|
|
33
|
+
return parseSuppressionPatterns(raw);
|
|
34
|
+
}
|
|
35
|
+
function isModelSuppressed(model, patterns) {
|
|
36
|
+
const envPatterns = getEnvSuppressedModels();
|
|
37
|
+
const explicitPatterns = parseSuppressionPatterns(patterns);
|
|
38
|
+
const allPatterns = [...envPatterns, ...explicitPatterns];
|
|
39
|
+
if (allPatterns.length === 0)
|
|
40
|
+
return false;
|
|
41
|
+
const id = model.id.toLowerCase();
|
|
42
|
+
const name = (model.name ?? "").toLowerCase();
|
|
43
|
+
const shortId = id.includes("/") ? id.split("/").pop() : id.includes(":") ? id.split(":").pop() : id;
|
|
44
|
+
for (const pattern of allPatterns) {
|
|
45
|
+
if (matchesPattern(id, name, shortId, pattern)) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
function matchesPattern(id, name, shortId, pattern) {
|
|
52
|
+
const p = pattern.toLowerCase();
|
|
53
|
+
if (p.startsWith("regex:")) {
|
|
54
|
+
try {
|
|
55
|
+
const re = new RegExp(pattern.slice(6), "i");
|
|
56
|
+
return re.test(id) || re.test(name) || re.test(shortId);
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (p.includes("*") || p.includes("?")) {
|
|
62
|
+
const regexStr = "^" + p.replace(/([.+^${}()|[\]\\])/g, "\\$1").replace(/\*/g, ".*").replace(/\?/g, ".") + "$";
|
|
63
|
+
try {
|
|
64
|
+
const re = new RegExp(regexStr, "i");
|
|
65
|
+
return re.test(id) || re.test(name) || re.test(shortId);
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return id === p || name === p || shortId === p;
|
|
71
|
+
}
|
|
18
72
|
// ../plexus-models/src/convert.ts
|
|
19
73
|
var REASONING_PARAMS = new Set(["reasoning", "include_reasoning", "reasoning_effort"]);
|
|
20
74
|
var NON_CHAT_PATTERN = /(?:^|[\W_])(?:embed(?:ding|dings)?|transcri(?:be[ds]?|ptions?)|whisper|speech[\W_]*to[\W_]*text|stt|text[\W_]*to[\W_]*speech|tts|image[\W_]*(?:gen(?:eration)?|\d+)|diffusion|dall[\W_]*e|stable[\W_]*diffusion|sdxl|dream)(?:$|[\W_])/i;
|
|
@@ -104,23 +158,27 @@ function fallbackDir() {
|
|
|
104
158
|
function getDir() {
|
|
105
159
|
return fallbackDir();
|
|
106
160
|
}
|
|
107
|
-
function filterCachedModels(models) {
|
|
108
|
-
return Object.fromEntries(Object.entries(models).filter(([, model]) =>
|
|
109
|
-
id: model.id,
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
161
|
+
function filterCachedModels(models, suppress) {
|
|
162
|
+
return Object.fromEntries(Object.entries(models).filter(([, model]) => {
|
|
163
|
+
if (isModelSuppressed({ id: model.id, name: model.name }, suppress))
|
|
164
|
+
return false;
|
|
165
|
+
return isChatModel({
|
|
166
|
+
id: model.id,
|
|
167
|
+
name: model.name,
|
|
168
|
+
architecture: {
|
|
169
|
+
input_modalities: model.modalities.input,
|
|
170
|
+
output_modalities: model.modalities.output
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}));
|
|
116
174
|
}
|
|
117
|
-
async function readCachedModels(_client) {
|
|
175
|
+
async function readCachedModels(_client, suppress) {
|
|
118
176
|
try {
|
|
119
177
|
const dir = getDir();
|
|
120
178
|
const content = await readFile(join(dir, CACHE_FILE), "utf8");
|
|
121
179
|
const parsed = JSON.parse(content);
|
|
122
180
|
if (parsed && typeof parsed.models === "object" && !Array.isArray(parsed.models)) {
|
|
123
|
-
return filterCachedModels(parsed.models);
|
|
181
|
+
return filterCachedModels(parsed.models, suppress);
|
|
124
182
|
}
|
|
125
183
|
return null;
|
|
126
184
|
} catch {
|
|
@@ -249,9 +307,24 @@ function resolveConfig(provider, authMetadata) {
|
|
|
249
307
|
const optBaseURL = resolveStringOption(provider?.options?.[PLEXUS_BASE_URL_OPTION]);
|
|
250
308
|
const legacyBaseURL = resolveStringOption(provider?.options?.baseURL);
|
|
251
309
|
const optApiKey = resolveStringOption(provider?.options?.apiKey);
|
|
310
|
+
const optSuppress = provider?.options?.["suppressModels"] ?? provider?.options?.["suppress"] ?? provider?.options?.["suppress_models"];
|
|
311
|
+
const envSuppress = getEnvSuppressedModels();
|
|
312
|
+
const configSuppress = parseSuppressionPatterns(optSuppress);
|
|
313
|
+
const suppressModels = [...envSuppress, ...configSuppress];
|
|
252
314
|
const baseURL = (envBaseURL ? rootURL(envBaseURL) : undefined) || (authBaseURL ? rootURL(authBaseURL) : undefined) || (optBaseURL ? rootURL(optBaseURL) : undefined) || (legacyBaseURL ? rootURL(legacyBaseURL) : undefined) || undefined;
|
|
253
315
|
const apiKey = (envApiKey ? envApiKey.trim() : undefined) || optApiKey || undefined;
|
|
254
|
-
return {
|
|
316
|
+
return {
|
|
317
|
+
baseURL: baseURL || undefined,
|
|
318
|
+
apiKey: apiKey || undefined,
|
|
319
|
+
...suppressModels.length > 0 ? { suppressModels } : {}
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
function getSuppressedModels(provider) {
|
|
323
|
+
const envSuppressed = getEnvSuppressedModels();
|
|
324
|
+
const providerObj = provider ?? {};
|
|
325
|
+
const providerOptions = typeof providerObj["options"] === "object" && providerObj["options"] !== null ? providerObj["options"] : {};
|
|
326
|
+
const optSuppressed = parseSuppressionPatterns(providerOptions["suppressModels"] ?? providerOptions["suppress"] ?? providerObj["suppressModels"] ?? providerObj["suppress"]);
|
|
327
|
+
return [...envSuppressed, ...optSuppressed];
|
|
255
328
|
}
|
|
256
329
|
|
|
257
330
|
// src/log.ts
|
|
@@ -353,11 +426,13 @@ function buildOutputModalities(model) {
|
|
|
353
426
|
}
|
|
354
427
|
return ["text"];
|
|
355
428
|
}
|
|
356
|
-
function buildModels(models, baseURL) {
|
|
429
|
+
function buildModels(models, baseURL, suppress) {
|
|
357
430
|
const result = {};
|
|
358
431
|
for (const m of models) {
|
|
359
432
|
if (!isChatModel(m))
|
|
360
433
|
continue;
|
|
434
|
+
if (isModelSuppressed(m, suppress))
|
|
435
|
+
continue;
|
|
361
436
|
const outputModalities = buildOutputModalities(m);
|
|
362
437
|
if (outputModalities === null)
|
|
363
438
|
continue;
|
|
@@ -497,7 +572,7 @@ function raceWithTimeout(promise, timeoutMs) {
|
|
|
497
572
|
});
|
|
498
573
|
});
|
|
499
574
|
}
|
|
500
|
-
function refreshModels(client, baseURL, log, apiKey, force = false) {
|
|
575
|
+
function refreshModels(client, baseURL, log, apiKey, force = false, suppress) {
|
|
501
576
|
if (!force && lastRefresh && Date.now() - lastRefresh.at < REFRESH_TTL_MS) {
|
|
502
577
|
log.info(`Using in-memory plexus model cache (${Object.keys(lastRefresh.models).length} models)`);
|
|
503
578
|
return Promise.resolve(lastRefresh.models);
|
|
@@ -507,7 +582,7 @@ function refreshModels(client, baseURL, log, apiKey, force = false) {
|
|
|
507
582
|
const run = async () => {
|
|
508
583
|
const url = modelsUrl(baseURL);
|
|
509
584
|
const { models: apiModels, raw } = await fetchPlexusModels(apiKey ?? "", url);
|
|
510
|
-
const built = buildModels(apiModels, apiBase(baseURL));
|
|
585
|
+
const built = buildModels(apiModels, apiBase(baseURL), suppress);
|
|
511
586
|
for (const [id, model] of Object.entries(built)) {
|
|
512
587
|
const providerNpm = model.provider?.npm ?? OPENAI_COMPATIBLE_NPM;
|
|
513
588
|
const providerApi = model.provider?.api ?? "(missing)";
|
|
@@ -531,15 +606,17 @@ var PlexusProviderPlugin = async (ctx) => {
|
|
|
531
606
|
const existing = cfg.provider[PLEXUS_PROVIDER_ID] ?? {};
|
|
532
607
|
const existingOptions = typeof existing["options"] === "object" && existing["options"] !== null ? existing["options"] : {};
|
|
533
608
|
const existingModels = typeof existing["models"] === "object" && existing["models"] !== null ? existing["models"] : null;
|
|
609
|
+
const suppress = getSuppressedModels(existing);
|
|
534
610
|
const { baseURL, apiKey } = resolveConfig(existing);
|
|
535
611
|
log.info(`Resolved plexus config: baseURL=${baseURL ?? "(missing)"} apiKey=${apiKey ? "present" : "missing"}`);
|
|
536
612
|
if (typeof existingOptions["baseURL"] === "string") {
|
|
537
613
|
log.warn(`Ignoring legacy provider.options.baseURL=${String(existingOptions["baseURL"])}`);
|
|
538
614
|
}
|
|
539
|
-
const cachedAsync = await readCachedModels(client);
|
|
615
|
+
const cachedAsync = await readCachedModels(client, suppress);
|
|
540
616
|
if (cachedAsync) {
|
|
541
617
|
log.info(`Loaded plexus cache with ${Object.keys(cachedAsync).length} models`);
|
|
542
618
|
}
|
|
619
|
+
const effectiveExistingModels = existingModels ? filterCachedModels(existingModels, suppress) : null;
|
|
543
620
|
const merged = {
|
|
544
621
|
...existing,
|
|
545
622
|
name: existing["name"] ?? PLEXUS_PROVIDER_NAME,
|
|
@@ -549,7 +626,7 @@ var PlexusProviderPlugin = async (ctx) => {
|
|
|
549
626
|
...baseURL ? { [PLEXUS_BASE_URL_OPTION]: baseURL } : {},
|
|
550
627
|
...apiKey ? { apiKey } : {}
|
|
551
628
|
},
|
|
552
|
-
models:
|
|
629
|
+
models: (effectiveExistingModels && Object.keys(effectiveExistingModels).length > 0 ? effectiveExistingModels : null) ?? (cachedAsync ? toConfigModels(cachedAsync) : null) ?? {
|
|
553
630
|
[PLACEHOLDER_MODEL_ID]: {
|
|
554
631
|
id: PLACEHOLDER_MODEL_ID,
|
|
555
632
|
name: "Plexus (run /connect to configure)",
|
|
@@ -584,21 +661,22 @@ var PlexusProviderPlugin = async (ctx) => {
|
|
|
584
661
|
provider: {
|
|
585
662
|
id: PLEXUS_PROVIDER_ID,
|
|
586
663
|
models: async (provider, hookCtx) => {
|
|
664
|
+
const suppress = getSuppressedModels(provider);
|
|
587
665
|
const authKey = hookCtx.auth?.type === "api" ? hookCtx.auth.key : undefined;
|
|
588
666
|
const { baseURL, apiKey } = resolveConfig(provider, authMetadata(hookCtx.auth));
|
|
589
667
|
const key = authKey ?? apiKey;
|
|
590
668
|
if (!baseURL) {
|
|
591
669
|
log.info("Provider hook skipped live refresh; baseURL missing");
|
|
592
|
-
const cached2 = await readCachedModels(client);
|
|
670
|
+
const cached2 = await readCachedModels(client, suppress);
|
|
593
671
|
return cached2 ? toRuntimeModels(cached2, provider) : {};
|
|
594
672
|
}
|
|
595
|
-
const refreshPromise = refreshModels(client, baseURL, log, key);
|
|
673
|
+
const refreshPromise = refreshModels(client, baseURL, log, key, false, suppress);
|
|
596
674
|
const race = await raceWithTimeout(refreshPromise, CONFIG_HOOK_REFRESH_BUDGET_MS);
|
|
597
675
|
if (race.status === "resolved") {
|
|
598
676
|
log.info(`Provider hook loaded ${Object.keys(race.value).length} plexus models from ${baseURL}`);
|
|
599
677
|
return toRuntimeModels(race.value, provider);
|
|
600
678
|
}
|
|
601
|
-
const cached = await readCachedModels(client);
|
|
679
|
+
const cached = await readCachedModels(client, suppress);
|
|
602
680
|
if (race.status === "rejected") {
|
|
603
681
|
log.warn(`Provider hook live refresh failed, using cache: ${String(race.error)}`);
|
|
604
682
|
} else {
|
|
@@ -615,6 +693,7 @@ var PlexusProviderPlugin = async (ctx) => {
|
|
|
615
693
|
return;
|
|
616
694
|
const configResponse = await client.config.get();
|
|
617
695
|
const provider = configResponse.data?.provider?.[PLEXUS_PROVIDER_ID];
|
|
696
|
+
const suppress = getSuppressedModels(provider);
|
|
618
697
|
const storedAuth = await readStoredAuth();
|
|
619
698
|
const { baseURL, apiKey } = resolveConfig(provider, storedAuth?.metadata);
|
|
620
699
|
const key = storedAuth?.key ?? apiKey;
|
|
@@ -623,7 +702,7 @@ var PlexusProviderPlugin = async (ctx) => {
|
|
|
623
702
|
}
|
|
624
703
|
let models;
|
|
625
704
|
try {
|
|
626
|
-
models = await refreshModels(client, baseURL, log, key, true);
|
|
705
|
+
models = await refreshModels(client, baseURL, log, key, true, suppress);
|
|
627
706
|
} catch (e) {
|
|
628
707
|
throw new Error(`Plexus refresh failed: ${String(e)}. Existing cache left untouched.`);
|
|
629
708
|
}
|
|
@@ -692,6 +771,7 @@ export {
|
|
|
692
771
|
buildModels,
|
|
693
772
|
REFRESH_TTL_MS,
|
|
694
773
|
PlexusProviderPlugin,
|
|
774
|
+
PLEXUS_SUPPRESS_MODELS_OPTION,
|
|
695
775
|
PLEXUS_REFRESH_COMMAND,
|
|
696
776
|
PLEXUS_PROVIDER_NAME,
|
|
697
777
|
PLEXUS_PROVIDER_ID,
|
|
@@ -701,6 +781,10 @@ export {
|
|
|
701
781
|
PLACEHOLDER_MODEL_ID,
|
|
702
782
|
OPENAI_COMPATIBLE_NPM,
|
|
703
783
|
MODELS_FETCH_TIMEOUT_MS,
|
|
784
|
+
ENV_SUPPRESS_MODELS,
|
|
785
|
+
ENV_SUPPRESSED_MODELS,
|
|
786
|
+
ENV_IGNORE_MODELS,
|
|
787
|
+
ENV_EXCLUDE_MODELS,
|
|
704
788
|
ENV_BASE_URL,
|
|
705
789
|
ENV_API_URL,
|
|
706
790
|
ENV_API_KEY,
|