@mcowger/opencode-plexus 0.8.2 → 0.8.4
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/dist/index.js +87 -81
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ var ENV_BASE_URL = "PLEXUS_BASE_URL";
|
|
|
10
10
|
var ENV_API_KEY = "PLEXUS_API_KEY";
|
|
11
11
|
var MODELS_FETCH_TIMEOUT_MS = 1e4;
|
|
12
12
|
var REFRESH_TTL_MS = 60000;
|
|
13
|
+
var CONFIG_HOOK_REFRESH_BUDGET_MS = 3000;
|
|
13
14
|
var PLACEHOLDER_MODEL_ID = "plexus-unconfigured";
|
|
14
15
|
|
|
15
16
|
// ../plexus-models/src/convert.ts
|
|
@@ -46,67 +47,48 @@ function adjustBaseUrl(baseUrl, preferredApi) {
|
|
|
46
47
|
return stripped;
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
var DEFAULT_MODELS_FETCH_TIMEOUT_MS = 1e4;
|
|
51
|
+
async function fetchPlexusModels(apiKey, modelsUrl, timeoutMs = DEFAULT_MODELS_FETCH_TIMEOUT_MS) {
|
|
52
|
+
const controller = new AbortController;
|
|
53
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
54
|
+
try {
|
|
55
|
+
const res = await fetch(modelsUrl, {
|
|
56
|
+
headers: {
|
|
57
|
+
Authorization: `Bearer ${apiKey}`,
|
|
58
|
+
Accept: "application/json"
|
|
59
|
+
},
|
|
60
|
+
signal: controller.signal
|
|
61
|
+
});
|
|
62
|
+
if (!res.ok) {
|
|
63
|
+
throw new Error(`Plexus models fetch failed: ${res.status} ${res.statusText}`);
|
|
54
64
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
65
|
+
const raw = await res.json();
|
|
66
|
+
return { models: raw.data ?? [], raw };
|
|
67
|
+
} catch (err) {
|
|
68
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
69
|
+
throw new Error(`Plexus models fetch timed out after ${timeoutMs}ms`);
|
|
70
|
+
}
|
|
71
|
+
throw err;
|
|
72
|
+
} finally {
|
|
73
|
+
clearTimeout(timer);
|
|
58
74
|
}
|
|
59
|
-
const raw = await res.json();
|
|
60
|
-
return { models: raw.data ?? [], raw };
|
|
61
75
|
}
|
|
62
76
|
// src/cache.ts
|
|
63
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
64
77
|
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
65
78
|
import { homedir } from "os";
|
|
66
79
|
import { join } from "path";
|
|
67
80
|
var PLUGIN_SUBDIR = join("plugins", "plexus");
|
|
68
81
|
var CACHE_FILE = "models-cache.json";
|
|
69
82
|
var RAW_FILE = "models-raw.json";
|
|
70
|
-
var resolvedDir = null;
|
|
71
83
|
function fallbackDir() {
|
|
72
84
|
return join(homedir(), ".local", "share", "opencode", PLUGIN_SUBDIR);
|
|
73
85
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
return resolvedDir;
|
|
77
|
-
try {
|
|
78
|
-
const res = await client.path.get();
|
|
79
|
-
const data = res?.data;
|
|
80
|
-
const state = typeof data?.state === "string" && data.state ? data.state : undefined;
|
|
81
|
-
if (state) {
|
|
82
|
-
resolvedDir = join(state, PLUGIN_SUBDIR);
|
|
83
|
-
return resolvedDir;
|
|
84
|
-
}
|
|
85
|
-
} catch {}
|
|
86
|
-
resolvedDir = fallbackDir();
|
|
87
|
-
return resolvedDir;
|
|
88
|
-
}
|
|
89
|
-
function syncCachePath() {
|
|
90
|
-
return join(fallbackDir(), CACHE_FILE);
|
|
91
|
-
}
|
|
92
|
-
function readCachedModelsSync() {
|
|
93
|
-
try {
|
|
94
|
-
const path = syncCachePath();
|
|
95
|
-
if (!existsSync(path))
|
|
96
|
-
return null;
|
|
97
|
-
const raw = readFileSync(path, "utf8");
|
|
98
|
-
const parsed = JSON.parse(raw);
|
|
99
|
-
if (parsed && typeof parsed.models === "object" && !Array.isArray(parsed.models)) {
|
|
100
|
-
return parsed.models;
|
|
101
|
-
}
|
|
102
|
-
return null;
|
|
103
|
-
} catch {
|
|
104
|
-
return null;
|
|
105
|
-
}
|
|
86
|
+
function getDir() {
|
|
87
|
+
return fallbackDir();
|
|
106
88
|
}
|
|
107
|
-
async function readCachedModels(
|
|
89
|
+
async function readCachedModels(_client) {
|
|
108
90
|
try {
|
|
109
|
-
const dir =
|
|
91
|
+
const dir = getDir();
|
|
110
92
|
const content = await readFile(join(dir, CACHE_FILE), "utf8");
|
|
111
93
|
const parsed = JSON.parse(content);
|
|
112
94
|
if (parsed && typeof parsed.models === "object" && !Array.isArray(parsed.models)) {
|
|
@@ -117,9 +99,9 @@ async function readCachedModels(client) {
|
|
|
117
99
|
return null;
|
|
118
100
|
}
|
|
119
101
|
}
|
|
120
|
-
async function writeCache(
|
|
102
|
+
async function writeCache(_client, models, raw) {
|
|
121
103
|
try {
|
|
122
|
-
const dir =
|
|
104
|
+
const dir = getDir();
|
|
123
105
|
await mkdir(dir, { recursive: true });
|
|
124
106
|
const cache = { models, timestamp: Date.now() };
|
|
125
107
|
await writeFile(join(dir, CACHE_FILE), JSON.stringify(cache, null, 2) + `
|
|
@@ -128,12 +110,6 @@ async function writeCache(client, models, raw) {
|
|
|
128
110
|
await writeFile(join(dir, RAW_FILE), JSON.stringify(raw, null, 2) + `
|
|
129
111
|
`, "utf8");
|
|
130
112
|
}
|
|
131
|
-
try {
|
|
132
|
-
const syncDir = fallbackDir();
|
|
133
|
-
mkdirSync(syncDir, { recursive: true });
|
|
134
|
-
writeFileSync(join(syncDir, CACHE_FILE), JSON.stringify(cache, null, 2) + `
|
|
135
|
-
`, "utf8");
|
|
136
|
-
} catch {}
|
|
137
113
|
} catch {}
|
|
138
114
|
}
|
|
139
115
|
|
|
@@ -313,6 +289,19 @@ function buildModels(models, baseURL) {
|
|
|
313
289
|
|
|
314
290
|
// src/plugin.ts
|
|
315
291
|
var lastRefresh = null;
|
|
292
|
+
var inFlightRefresh = null;
|
|
293
|
+
function raceWithTimeout(promise, timeoutMs) {
|
|
294
|
+
return new Promise((resolve) => {
|
|
295
|
+
const timer = setTimeout(() => resolve({ status: "timed-out" }), timeoutMs);
|
|
296
|
+
promise.then((value) => {
|
|
297
|
+
clearTimeout(timer);
|
|
298
|
+
resolve({ status: "resolved", value });
|
|
299
|
+
}, (error) => {
|
|
300
|
+
clearTimeout(timer);
|
|
301
|
+
resolve({ status: "rejected", error });
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
}
|
|
316
305
|
function mergeModelMaps(base, overrides) {
|
|
317
306
|
if (!overrides)
|
|
318
307
|
return base;
|
|
@@ -348,22 +337,30 @@ function mergeModelMaps(base, overrides) {
|
|
|
348
337
|
}
|
|
349
338
|
return merged;
|
|
350
339
|
}
|
|
351
|
-
|
|
340
|
+
function refreshModels(client, baseURL, log, apiKey) {
|
|
352
341
|
if (lastRefresh && Date.now() - lastRefresh.at < REFRESH_TTL_MS) {
|
|
353
342
|
log.info(`Using in-memory plexus model cache (${Object.keys(lastRefresh.models).length} models)`);
|
|
354
|
-
return lastRefresh.models;
|
|
355
|
-
}
|
|
356
|
-
const url = modelsUrl(baseURL);
|
|
357
|
-
const { models: apiModels, raw } = await fetchPlexusModels(apiKey ?? "", url);
|
|
358
|
-
const built = buildModels(apiModels, apiBase(baseURL));
|
|
359
|
-
for (const [id, model] of Object.entries(built)) {
|
|
360
|
-
const providerNpm = model.provider?.npm ?? OPENAI_COMPATIBLE_NPM;
|
|
361
|
-
const providerApi = model.provider?.api ?? "(missing)";
|
|
362
|
-
log.info(`Model mapping ${id}: npm=${providerNpm} api=${providerApi}`);
|
|
343
|
+
return Promise.resolve(lastRefresh.models);
|
|
363
344
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
345
|
+
if (inFlightRefresh)
|
|
346
|
+
return inFlightRefresh;
|
|
347
|
+
const run = async () => {
|
|
348
|
+
const url = modelsUrl(baseURL);
|
|
349
|
+
const { models: apiModels, raw } = await fetchPlexusModels(apiKey ?? "", url);
|
|
350
|
+
const built = buildModels(apiModels, apiBase(baseURL));
|
|
351
|
+
for (const [id, model] of Object.entries(built)) {
|
|
352
|
+
const providerNpm = model.provider?.npm ?? OPENAI_COMPATIBLE_NPM;
|
|
353
|
+
const providerApi = model.provider?.api ?? "(missing)";
|
|
354
|
+
log.info(`Model mapping ${id}: npm=${providerNpm} api=${providerApi}`);
|
|
355
|
+
}
|
|
356
|
+
lastRefresh = { at: Date.now(), models: built };
|
|
357
|
+
writeCache(client, built, raw).catch(() => {});
|
|
358
|
+
return built;
|
|
359
|
+
};
|
|
360
|
+
inFlightRefresh = run().finally(() => {
|
|
361
|
+
inFlightRefresh = null;
|
|
362
|
+
});
|
|
363
|
+
return inFlightRefresh;
|
|
367
364
|
}
|
|
368
365
|
var PlexusProviderPlugin = async (ctx) => {
|
|
369
366
|
const { client } = ctx;
|
|
@@ -379,9 +376,9 @@ var PlexusProviderPlugin = async (ctx) => {
|
|
|
379
376
|
if (typeof existingOptions["baseURL"] === "string") {
|
|
380
377
|
log.warn(`Ignoring legacy provider.options.baseURL=${String(existingOptions["baseURL"])}`);
|
|
381
378
|
}
|
|
382
|
-
const
|
|
383
|
-
if (
|
|
384
|
-
log.info(`Loaded
|
|
379
|
+
const cachedAsync = await readCachedModels(client);
|
|
380
|
+
if (cachedAsync) {
|
|
381
|
+
log.info(`Loaded plexus cache with ${Object.keys(cachedAsync).length} models`);
|
|
385
382
|
}
|
|
386
383
|
const merged = {
|
|
387
384
|
...existing,
|
|
@@ -392,7 +389,7 @@ var PlexusProviderPlugin = async (ctx) => {
|
|
|
392
389
|
...baseURL ? { [PLEXUS_BASE_URL_OPTION]: baseURL } : {},
|
|
393
390
|
...apiKey ? { apiKey } : {}
|
|
394
391
|
},
|
|
395
|
-
models: existingModels ??
|
|
392
|
+
models: existingModels ?? cachedAsync ?? {
|
|
396
393
|
[PLACEHOLDER_MODEL_ID]: {
|
|
397
394
|
id: PLACEHOLDER_MODEL_ID,
|
|
398
395
|
name: "Plexus (run /connect to configure)",
|
|
@@ -404,16 +401,24 @@ var PlexusProviderPlugin = async (ctx) => {
|
|
|
404
401
|
const mergedOptions = merged["options"];
|
|
405
402
|
delete mergedOptions["baseURL"];
|
|
406
403
|
if (baseURL) {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
if (
|
|
415
|
-
merged["models"] = mergeModelMaps(
|
|
404
|
+
const refreshPromise = refreshModels(client, baseURL, log, apiKey);
|
|
405
|
+
const race = await raceWithTimeout(refreshPromise, CONFIG_HOOK_REFRESH_BUDGET_MS);
|
|
406
|
+
if (race.status === "resolved") {
|
|
407
|
+
merged["models"] = mergeModelMaps(race.value, existingModels);
|
|
408
|
+
log.info(`Loaded ${Object.keys(race.value).length} plexus models from ${baseURL}`);
|
|
409
|
+
} else if (race.status === "rejected") {
|
|
410
|
+
log.warn(`Live model refresh failed, using cache: ${String(race.error)}`);
|
|
411
|
+
if (cachedAsync) {
|
|
412
|
+
merged["models"] = mergeModelMaps(cachedAsync, existingModels);
|
|
413
|
+
}
|
|
414
|
+
} else {
|
|
415
|
+
log.info(`Live model refresh still pending after ${CONFIG_HOOK_REFRESH_BUDGET_MS}ms; using cache and continuing in background`);
|
|
416
|
+
if (cachedAsync) {
|
|
417
|
+
merged["models"] = mergeModelMaps(cachedAsync, existingModels);
|
|
416
418
|
}
|
|
419
|
+
refreshPromise.catch((e) => {
|
|
420
|
+
log.warn(`Background plexus model refresh failed: ${String(e)}`);
|
|
421
|
+
});
|
|
417
422
|
}
|
|
418
423
|
} else {
|
|
419
424
|
log.info("Plexus baseURL not configured; skipping live refresh");
|
|
@@ -499,5 +504,6 @@ export {
|
|
|
499
504
|
OPENAI_COMPATIBLE_NPM,
|
|
500
505
|
MODELS_FETCH_TIMEOUT_MS,
|
|
501
506
|
ENV_BASE_URL,
|
|
502
|
-
ENV_API_KEY
|
|
507
|
+
ENV_API_KEY,
|
|
508
|
+
CONFIG_HOOK_REFRESH_BUDGET_MS
|
|
503
509
|
};
|