@mcowger/opencode-plexus 1.3.7 → 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.
Files changed (3) hide show
  1. package/README.md +42 -0
  2. package/dist/index.js +105 -21
  3. 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]) => isChatModel({
109
- id: model.id,
110
- name: model.name,
111
- architecture: {
112
- input_modalities: model.modalities.input,
113
- output_modalities: model.modalities.output
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 { baseURL: baseURL || undefined, apiKey: apiKey || undefined };
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: existingModels ?? (cachedAsync ? toConfigModels(cachedAsync) : null) ?? {
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcowger/opencode-plexus",
3
- "version": "1.3.7",
3
+ "version": "1.3.9",
4
4
  "description": "OpenCode plugin: Plexus provider with dynamic model discovery",
5
5
  "type": "module",
6
6
  "module": "./dist/index.js",