@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 +7 -3
- package/dist/index.js +63 -4
- package/package.json +1 -1
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.
|
|
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,
|
|
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
|
|
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;
|
|
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,
|