@mcowger/opencode-plexus 1.1.1 → 1.2.0

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
@@ -116,7 +116,11 @@ Run inside OpenCode:
116
116
  /connect
117
117
  ```
118
118
 
119
- Select **Plexus** and enter your base URL and API key. Models are discovered through OpenCode's provider hook and cached for fast startup on subsequent sessions.
119
+ Select **Plexus** and enter your base URL and API key. OpenCode has no live model-discovery hook for custom providers, so the model list is seeded from the on-disk cache at startup. Run `/plexus-refresh` to force a live fetch from Plexus and rewrite the cache, then restart OpenCode to pick up the refreshed list:
120
+
121
+ ```
122
+ /plexus-refresh
123
+ ```
120
124
 
121
125
  You may enter either the Plexus root URL or the `/v1` API base URL:
122
126
 
@@ -197,7 +201,7 @@ packages/
197
201
  plugin.ts # Plugin export: config hook, provider hook, auth handler
198
202
  mapper.ts # PlexusApiModel → OpenCode ConfigModel
199
203
  cache.ts # model cache I/O
200
- config-store.ts # resolveConfig, persistToGlobalConfig
204
+ config-store.ts # resolveConfig, readStoredAuth
201
205
  log.ts # logger via OpenCode SDK
202
206
  constants.ts # provider ID, env var names, timeouts
203
207
  url.ts # URL helpers (trimURL, apiBase, modelsUrl)
@@ -228,7 +232,7 @@ Models with a falsy `id` are skipped. Missing metadata falls back to safe defaul
228
232
  ## Adapter behavior
229
233
 
230
234
  - **pi** refreshes on session start and through `/plexus refresh`. It accepts either root URLs or URLs ending in `/v1` and normalizes them before calling Plexus.
231
- - **OpenCode** seeds the provider from cache or a placeholder model during config loading, then performs live discovery through the `provider.models` hook. If Plexus is slow or unavailable, OpenCode uses the cache and lets the refresh continue in the background.
235
+ - **OpenCode** seeds the provider from the on-disk cache (or a placeholder model) once, during config loading OpenCode's `provider.models` hook never fires for custom providers, so there is no live discovery at startup. Run `/plexus-refresh` to force a live fetch and rewrite the cache; because OpenCode has no way to hot-reload a custom provider's model list mid-session, a restart is required afterward to see the refreshed models in the picker.
232
236
  - OpenCode models retain their upstream model ID, SDK dialect, release date, and reasoning metadata so OpenCode can generate its native GPT, Claude, Gemini, and OpenAI-compatible variants and apply its current request transforms. DeepSeek models also preserve `reasoning_content` across tool-call turns.
233
237
  - OpenCode uses a 250K-token context window when Plexus supplies no context metadata; its output fallback remains 20% of that window.
234
238
  - Both adapters convert Plexus's per-token base and tier rates to the per-million-token units expected by their host.
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@ var MODELS_FETCH_TIMEOUT_MS = 1e4;
13
13
  var REFRESH_TTL_MS = 60000;
14
14
  var CONFIG_HOOK_REFRESH_BUDGET_MS = 3000;
15
15
  var PLACEHOLDER_MODEL_ID = "plexus-unconfigured";
16
+ var PLEXUS_REFRESH_COMMAND = "plexus-refresh";
16
17
 
17
18
  // ../plexus-models/src/convert.ts
18
19
  var REASONING_PARAMS = new Set(["reasoning", "include_reasoning", "reasoning_effort"]);
@@ -103,13 +104,23 @@ function fallbackDir() {
103
104
  function getDir() {
104
105
  return fallbackDir();
105
106
  }
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
+ })));
116
+ }
106
117
  async function readCachedModels(_client) {
107
118
  try {
108
119
  const dir = getDir();
109
120
  const content = await readFile(join(dir, CACHE_FILE), "utf8");
110
121
  const parsed = JSON.parse(content);
111
122
  if (parsed && typeof parsed.models === "object" && !Array.isArray(parsed.models)) {
112
- return parsed.models;
123
+ return filterCachedModels(parsed.models);
113
124
  }
114
125
  return null;
115
126
  } catch {
@@ -130,6 +141,11 @@ async function writeCache(_client, models, raw) {
130
141
  } catch {}
131
142
  }
132
143
 
144
+ // src/config-store.ts
145
+ import { readFile as readFile2 } from "fs/promises";
146
+ import { homedir as homedir2 } from "os";
147
+ import { join as join2 } from "path";
148
+
133
149
  // src/url.ts
134
150
  function trimURL(s) {
135
151
  return s.trim().replace(/\/+$/, "");
@@ -155,6 +171,21 @@ function modelsUrl(baseURL) {
155
171
  var ENV_VAR_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
156
172
  var ENV_VAR_NAME_PREFIX_RE = /^[A-Za-z_][A-Za-z0-9_]*/;
157
173
  var AUTH_METADATA_BASE_URL = "plexusBaseURL";
174
+ async function readStoredAuth() {
175
+ const dataHome = process.env.XDG_DATA_HOME || join2(homedir2(), ".local", "share");
176
+ const file = join2(dataHome, "opencode", "auth.json");
177
+ try {
178
+ const content = await readFile2(file, "utf8");
179
+ const parsed = JSON.parse(content);
180
+ const entry = parsed[PLEXUS_PROVIDER_ID];
181
+ if (typeof entry === "object" && entry !== null && entry.type === "api" && typeof entry.key === "string") {
182
+ return entry;
183
+ }
184
+ return;
185
+ } catch {
186
+ return;
187
+ }
188
+ }
158
189
  function resolveConfigTemplate(value) {
159
190
  let result = "";
160
191
  let index = 0;
@@ -466,8 +497,8 @@ function raceWithTimeout(promise, timeoutMs) {
466
497
  });
467
498
  });
468
499
  }
469
- function refreshModels(client, baseURL, log, apiKey) {
470
- if (lastRefresh && Date.now() - lastRefresh.at < REFRESH_TTL_MS) {
500
+ function refreshModels(client, baseURL, log, apiKey, force = false) {
501
+ if (!force && lastRefresh && Date.now() - lastRefresh.at < REFRESH_TTL_MS) {
471
502
  log.info(`Using in-memory plexus model cache (${Object.keys(lastRefresh.models).length} models)`);
472
503
  return Promise.resolve(lastRefresh.models);
473
504
  }
@@ -530,10 +561,15 @@ var PlexusProviderPlugin = async (ctx) => {
530
561
  const mergedOptions = merged["options"];
531
562
  delete mergedOptions["baseURL"];
532
563
  if (baseURL) {
533
- log.info("Plexus baseURL configured; live discovery delegated to provider.models hook");
564
+ log.info("Plexus baseURL configured; run /plexus-refresh to force a live model refresh");
534
565
  } else {
535
566
  log.info("Plexus baseURL not configured; skipping live refresh");
536
567
  }
568
+ cfg.command ??= {};
569
+ cfg.command[PLEXUS_REFRESH_COMMAND] ??= {
570
+ template: "Refreshing Plexus models from the live server...",
571
+ description: "Force a live refresh of Plexus models and rewrite the on-disk cache"
572
+ };
537
573
  try {
538
574
  const mergedModels = merged["models"];
539
575
  for (const id of ["gemini-3.5-flash", "claude-haiku-4-5", "small-fast"]) {
@@ -574,6 +610,28 @@ var PlexusProviderPlugin = async (ctx) => {
574
610
  return cached ? toRuntimeModels(cached, provider) : {};
575
611
  }
576
612
  },
613
+ "command.execute.before": async (input, output) => {
614
+ if (input.command !== PLEXUS_REFRESH_COMMAND)
615
+ return;
616
+ const configResponse = await client.config.get();
617
+ const provider = configResponse.data?.provider?.[PLEXUS_PROVIDER_ID];
618
+ const storedAuth = await readStoredAuth();
619
+ const { baseURL, apiKey } = resolveConfig(provider, storedAuth?.metadata);
620
+ const key = storedAuth?.key ?? apiKey;
621
+ const textPart = (text) => ({ type: "text", text, synthetic: true });
622
+ if (!baseURL) {
623
+ output.parts = [textPart("Plexus refresh failed: no baseURL configured. Run /connect first.")];
624
+ return;
625
+ }
626
+ try {
627
+ const models = await refreshModels(client, baseURL, log, key, true);
628
+ output.parts = [
629
+ textPart(`Plexus models refreshed: ${Object.keys(models).length} models fetched from ${baseURL} and cached. Restart OpenCode to pick up the new model list.`)
630
+ ];
631
+ } catch (e) {
632
+ output.parts = [textPart(`Plexus refresh failed: ${String(e)}. Existing cache left untouched.`)];
633
+ }
634
+ },
577
635
  auth: {
578
636
  provider: PLEXUS_PROVIDER_ID,
579
637
  async loader(getAuth, providerInfo) {
@@ -637,6 +695,7 @@ export {
637
695
  buildModels,
638
696
  REFRESH_TTL_MS,
639
697
  PlexusProviderPlugin,
698
+ PLEXUS_REFRESH_COMMAND,
640
699
  PLEXUS_PROVIDER_NAME,
641
700
  PLEXUS_PROVIDER_ID,
642
701
  PLEXUS_PLUGIN_ID,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcowger/opencode-plexus",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "OpenCode plugin: Plexus provider with dynamic model discovery",
5
5
  "type": "module",
6
6
  "module": "./dist/index.js",