@mcowger/oh-my-pi-plexus 1.3.8 → 1.3.10

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 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/extension.js CHANGED
@@ -109228,6 +109228,55 @@ var init_devin = __esm(() => {
109228
109228
  NO_REASONING_LABEL_PATTERN = /\bno thinking\b/i;
109229
109229
  });
109230
109230
 
109231
+ // ../plexus-models/src/suppress.ts
109232
+ function parseSuppressionPatterns(raw) {
109233
+ if (!raw)
109234
+ return [];
109235
+ const items = Array.isArray(raw) ? raw : raw.split(/[\n,;]+/);
109236
+ return items.map((s) => s.trim()).filter((s) => s.length > 0);
109237
+ }
109238
+ function getEnvSuppressedModels() {
109239
+ const env = typeof process !== "undefined" && process?.env ? process.env : {};
109240
+ const raw = env.PLEXUS_SUPPRESS_MODELS ?? env.PLEXUS_EXCLUDE_MODELS;
109241
+ return parseSuppressionPatterns(raw);
109242
+ }
109243
+ function isModelSuppressed(model, patterns) {
109244
+ const envPatterns = getEnvSuppressedModels();
109245
+ const explicitPatterns = parseSuppressionPatterns(patterns);
109246
+ const allPatterns = [...envPatterns, ...explicitPatterns];
109247
+ if (allPatterns.length === 0)
109248
+ return false;
109249
+ const id = model.id.toLowerCase();
109250
+ const name = (model.name ?? "").toLowerCase();
109251
+ const shortId = id.includes("/") ? id.split("/").pop() : id.includes(":") ? id.split(":").pop() : id;
109252
+ for (const pattern of allPatterns) {
109253
+ if (matchesPattern(id, name, shortId, pattern)) {
109254
+ return true;
109255
+ }
109256
+ }
109257
+ return false;
109258
+ }
109259
+ function matchesPattern(id, name, shortId, pattern) {
109260
+ const p = pattern.toLowerCase();
109261
+ if (p.startsWith("regex:")) {
109262
+ try {
109263
+ const re = new RegExp(pattern.slice(6), "i");
109264
+ return re.test(id) || re.test(name) || re.test(shortId);
109265
+ } catch {
109266
+ return false;
109267
+ }
109268
+ }
109269
+ if (p.includes("*") || p.includes("?")) {
109270
+ const regexStr = "^" + p.replace(/([.+^${}()|[\]\\])/g, "\\$1").replace(/\*/g, ".*").replace(/\?/g, ".") + "$";
109271
+ try {
109272
+ const re = new RegExp(regexStr, "i");
109273
+ return re.test(id) || re.test(name) || re.test(shortId);
109274
+ } catch {
109275
+ return false;
109276
+ }
109277
+ }
109278
+ return id === p || name === p || shortId === p;
109279
+ }
109231
109280
  // ../plexus-models/src/convert.ts
109232
109281
  var REASONING_PARAMS = new Set(["reasoning", "include_reasoning", "reasoning_effort"]);
109233
109282
  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;
@@ -109361,11 +109410,68 @@ function isChatModel(model) {
109361
109410
  const apiHints = Array.isArray(model.preferred_api) ? model.preferred_api.join(" ") : model.preferred_api ?? "";
109362
109411
  return !NON_CHAT_PATTERN.test(`${model.id} ${model.name ?? ""} ${apiHints}`);
109363
109412
  }
109364
- function convertDescriptors(models, baseUrl) {
109413
+ function parseSuppressionPatterns2(input) {
109414
+ if (!input)
109415
+ return [];
109416
+ const rawItems = Array.isArray(input) ? input : [input];
109417
+ const patterns = [];
109418
+ for (const item of rawItems) {
109419
+ if (!item || typeof item !== "string")
109420
+ continue;
109421
+ const parts = item.split(/[\n,]/);
109422
+ for (const part of parts) {
109423
+ const trimmed = part.trim();
109424
+ if (trimmed.length > 0) {
109425
+ patterns.push(trimmed);
109426
+ }
109427
+ }
109428
+ }
109429
+ return patterns;
109430
+ }
109431
+ function isSuppressedModel(model, suppress) {
109432
+ if (!model || !model.id)
109433
+ return false;
109434
+ const patterns = parseSuppressionPatterns2(suppress);
109435
+ if (patterns.length === 0)
109436
+ return false;
109437
+ const idLower = model.id.toLowerCase();
109438
+ const nameLower = (model.name ?? "").toLowerCase();
109439
+ for (const pattern of patterns) {
109440
+ const patternLower = pattern.toLowerCase();
109441
+ if (idLower === patternLower || nameLower && nameLower === patternLower) {
109442
+ return true;
109443
+ }
109444
+ if (pattern.startsWith("/") && pattern.lastIndexOf("/") > 0) {
109445
+ const lastSlash = pattern.lastIndexOf("/");
109446
+ const regexBody = pattern.slice(1, lastSlash);
109447
+ const regexFlags = pattern.slice(lastSlash + 1) || "i";
109448
+ try {
109449
+ const re = new RegExp(regexBody, regexFlags);
109450
+ if (re.test(model.id) || model.name && re.test(model.name)) {
109451
+ return true;
109452
+ }
109453
+ } catch {}
109454
+ }
109455
+ if (pattern.includes("*") || pattern.includes("?")) {
109456
+ try {
109457
+ const escaped = patternLower.replace(/[.+^$()|[{}]\\]/g, "\\$&");
109458
+ const regexStr = "^" + escaped.replace(/\*/g, ".*").replace(/\?/g, ".") + "$";
109459
+ const globRe = new RegExp(regexStr, "i");
109460
+ if (globRe.test(model.id) || model.name && globRe.test(model.name)) {
109461
+ return true;
109462
+ }
109463
+ } catch {}
109464
+ }
109465
+ }
109466
+ return false;
109467
+ }
109468
+ function convertDescriptors(models, baseUrl, suppress) {
109365
109469
  const result = [];
109366
109470
  for (const m of models) {
109367
109471
  if (!isChatModel(m))
109368
109472
  continue;
109473
+ if (isSuppressedModel(m, suppress))
109474
+ continue;
109369
109475
  result.push(convertToDescriptor(m, baseUrl));
109370
109476
  }
109371
109477
  return result;
@@ -109430,22 +109536,28 @@ function detectOpenAICompletionsCompat(providerName, baseUrl) {
109430
109536
  return compat;
109431
109537
  }
109432
109538
  var DEFAULT_MODELS_FETCH_TIMEOUT_MS = 1e4;
109433
- async function fetchPlexusModels(apiKey, modelsUrl, timeoutMs = DEFAULT_MODELS_FETCH_TIMEOUT_MS) {
109539
+ async function fetchPlexusModels(apiKey, modelsUrl, timeoutMs = DEFAULT_MODELS_FETCH_TIMEOUT_MS, etag) {
109434
109540
  const controller = new AbortController;
109435
109541
  const timer = setTimeout(() => controller.abort(), timeoutMs);
109436
109542
  try {
109437
109543
  const headers = { Accept: "application/json" };
109438
109544
  if (apiKey)
109439
109545
  headers.Authorization = `Bearer ${apiKey}`;
109546
+ if (etag)
109547
+ headers["If-None-Match"] = etag;
109440
109548
  const res = await fetch(modelsUrl, {
109441
109549
  headers,
109442
109550
  signal: controller.signal
109443
109551
  });
109552
+ if (res.status === 304) {
109553
+ return { models: [], notModified: true };
109554
+ }
109444
109555
  if (!res.ok) {
109445
109556
  throw new Error(`Plexus models fetch failed: ${res.status} ${res.statusText}`);
109446
109557
  }
109447
109558
  const raw = await res.json();
109448
- return { models: raw.data ?? [], raw };
109559
+ const responseEtag = res.headers.get("etag") ?? undefined;
109560
+ return { models: raw.data ?? [], raw, etag: responseEtag };
109449
109561
  } catch (err) {
109450
109562
  if (err instanceof Error && err.name === "AbortError") {
109451
109563
  throw new Error(`Plexus models fetch timed out after ${timeoutMs}ms`);
@@ -109579,6 +109691,12 @@ function getBaseUrl() {
109579
109691
  const raw = getRawBaseUrl();
109580
109692
  return raw ? normalizeApiBase(raw) : null;
109581
109693
  }
109694
+ function getSuppressedModels() {
109695
+ const config = getConfigSync();
109696
+ const envSuppressed = getEnvSuppressedModels();
109697
+ const configSuppressed = parseSuppressionPatterns(config.suppressModels ?? config.suppress);
109698
+ return [...envSuppressed, ...configSuppressed];
109699
+ }
109582
109700
 
109583
109701
  // src/cache.ts
109584
109702
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
@@ -109598,7 +109716,8 @@ function parseCacheData(raw) {
109598
109716
  return null;
109599
109717
  return {
109600
109718
  models: obj["models"],
109601
- timestamp: typeof obj["timestamp"] === "number" ? obj["timestamp"] : 0
109719
+ timestamp: typeof obj["timestamp"] === "number" ? obj["timestamp"] : 0,
109720
+ etag: typeof obj["etag"] === "string" ? obj["etag"] : undefined
109602
109721
  };
109603
109722
  } catch {
109604
109723
  return null;
@@ -109614,9 +109733,9 @@ function readCachedModelsSync() {
109614
109733
  return null;
109615
109734
  }
109616
109735
  }
109617
- async function writeCachedModels(models) {
109736
+ async function writeCachedModels(models, etag) {
109618
109737
  await mkdir2(getCacheDir(), { recursive: true });
109619
- const payload = { models, timestamp: Date.now() };
109738
+ const payload = { models, timestamp: Date.now(), etag };
109620
109739
  await writeFile2(getModelsCachePath(), `${JSON.stringify(payload, null, 2)}
109621
109740
  `, "utf8");
109622
109741
  }
@@ -129293,8 +129412,9 @@ function getProviderApiKeyConfig() {
129293
129412
  var currentModels = [];
129294
129413
  function plexusExtension(pi) {
129295
129414
  const cached3 = readCachedModelsSync();
129415
+ const suppressPatterns = getSuppressedModels();
129296
129416
  const startupBaseUrl = getBaseUrl() ?? "http://localhost/v1";
129297
- const startupModels = cached3?.models.map(descriptorToOhMyPiModel) ?? [];
129417
+ const startupModels = (cached3?.models ?? []).filter((m) => !isModelSuppressed({ id: m.id, name: m.name }, suppressPatterns)).map(descriptorToOhMyPiModel);
129298
129418
  log("startup", {
129299
129419
  cachedModelCount: startupModels.length,
129300
129420
  startupBaseUrl
@@ -129416,10 +129536,18 @@ async function doRefresh(pi, apiKey, ctx) {
129416
129536
  return;
129417
129537
  }
129418
129538
  try {
129419
- const { models: apiModels, raw } = await fetchPlexusModels(apiKey, modelsUrl);
129420
- const descriptors3 = convertDescriptors(apiModels, baseUrl);
129539
+ const cached3 = readCachedModelsSync();
129540
+ const { models: apiModels, raw, etag, notModified } = await fetchPlexusModels(apiKey, modelsUrl, undefined, cached3?.etag);
129541
+ if (notModified) {
129542
+ log("doRefresh: not modified", { etag: cached3?.etag });
129543
+ if (ctx)
129544
+ ctx.ui.notify(`Refreshed ${currentModels.length} Plexus models (not modified)`, "info");
129545
+ return;
129546
+ }
129547
+ const suppressPatterns = getSuppressedModels();
129548
+ const descriptors3 = convertDescriptors(apiModels, baseUrl, suppressPatterns);
129421
129549
  const ohMyPiModels = descriptors3.map(descriptorToOhMyPiModel);
129422
- await Promise.all([writeCachedModels(descriptors3), writeRawResponse(raw)]);
129550
+ await Promise.all([writeCachedModels(descriptors3, etag), raw ? writeRawResponse(raw) : Promise.resolve()]);
129423
129551
  currentModels = ohMyPiModels;
129424
129552
  pi.registerProvider(PROVIDER_NAME, {
129425
129553
  api: "openai-completions",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcowger/oh-my-pi-plexus",
3
- "version": "1.3.8",
3
+ "version": "1.3.10",
4
4
  "description": "Plexus AI proxy plugin for the Oh My Pi coding agent",
5
5
  "type": "module",
6
6
  "files": [