@mcowger/opencode-plexus 1.1.2 → 1.2.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/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"]);
@@ -140,6 +141,11 @@ async function writeCache(_client, models, raw) {
140
141
  } catch {}
141
142
  }
142
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
+
143
149
  // src/url.ts
144
150
  function trimURL(s) {
145
151
  return s.trim().replace(/\/+$/, "");
@@ -165,6 +171,21 @@ function modelsUrl(baseURL) {
165
171
  var ENV_VAR_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
166
172
  var ENV_VAR_NAME_PREFIX_RE = /^[A-Za-z_][A-Za-z0-9_]*/;
167
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
+ }
168
189
  function resolveConfigTemplate(value) {
169
190
  let result = "";
170
191
  let index = 0;
@@ -476,8 +497,8 @@ function raceWithTimeout(promise, timeoutMs) {
476
497
  });
477
498
  });
478
499
  }
479
- function refreshModels(client, baseURL, log, apiKey) {
480
- 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) {
481
502
  log.info(`Using in-memory plexus model cache (${Object.keys(lastRefresh.models).length} models)`);
482
503
  return Promise.resolve(lastRefresh.models);
483
504
  }
@@ -540,10 +561,15 @@ var PlexusProviderPlugin = async (ctx) => {
540
561
  const mergedOptions = merged["options"];
541
562
  delete mergedOptions["baseURL"];
542
563
  if (baseURL) {
543
- 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");
544
565
  } else {
545
566
  log.info("Plexus baseURL not configured; skipping live refresh");
546
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
+ };
547
573
  try {
548
574
  const mergedModels = merged["models"];
549
575
  for (const id of ["gemini-3.5-flash", "claude-haiku-4-5", "small-fast"]) {
@@ -584,6 +610,25 @@ var PlexusProviderPlugin = async (ctx) => {
584
610
  return cached ? toRuntimeModels(cached, provider) : {};
585
611
  }
586
612
  },
613
+ "command.execute.before": async (input) => {
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
+ if (!baseURL) {
622
+ throw new Error("Plexus refresh failed: no baseURL configured. Run /connect first.");
623
+ }
624
+ let models;
625
+ try {
626
+ models = await refreshModels(client, baseURL, log, key, true);
627
+ } catch (e) {
628
+ throw new Error(`Plexus refresh failed: ${String(e)}. Existing cache left untouched.`);
629
+ }
630
+ throw new Error(`Plexus models refreshed: ${Object.keys(models).length} models fetched from ${baseURL} and cached. Restart OpenCode to pick up the new model list.`);
631
+ },
587
632
  auth: {
588
633
  provider: PLEXUS_PROVIDER_ID,
589
634
  async loader(getAuth, providerInfo) {
@@ -647,6 +692,7 @@ export {
647
692
  buildModels,
648
693
  REFRESH_TTL_MS,
649
694
  PlexusProviderPlugin,
695
+ PLEXUS_REFRESH_COMMAND,
650
696
  PLEXUS_PROVIDER_NAME,
651
697
  PLEXUS_PROVIDER_ID,
652
698
  PLEXUS_PLUGIN_ID,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcowger/opencode-plexus",
3
- "version": "1.1.2",
3
+ "version": "1.2.1",
4
4
  "description": "OpenCode plugin: Plexus provider with dynamic model discovery",
5
5
  "type": "module",
6
6
  "module": "./dist/index.js",