@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.
Files changed (2) hide show
  1. package/dist/index.js +87 -81
  2. 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
- async function fetchPlexusModels(apiKey, modelsUrl) {
50
- const res = await fetch(modelsUrl, {
51
- headers: {
52
- Authorization: `Bearer ${apiKey}`,
53
- Accept: "application/json"
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
- if (!res.ok) {
57
- throw new Error(`Plexus models fetch failed: ${res.status} ${res.statusText}`);
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
- async function getDir(client) {
75
- if (resolvedDir)
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(client) {
89
+ async function readCachedModels(_client) {
108
90
  try {
109
- const dir = await getDir(client);
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(client, models, raw) {
102
+ async function writeCache(_client, models, raw) {
121
103
  try {
122
- const dir = await getDir(client);
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
- async function refreshModels(client, baseURL, log, apiKey) {
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
- lastRefresh = { at: Date.now(), models: built };
365
- writeCache(client, built, raw).catch(() => {});
366
- return built;
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 cachedSync = readCachedModelsSync();
383
- if (cachedSync) {
384
- log.info(`Loaded sync plexus cache with ${Object.keys(cachedSync).length} models`);
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 ?? cachedSync ?? {
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
- try {
408
- const built = await refreshModels(client, baseURL, log, apiKey);
409
- merged["models"] = mergeModelMaps(built, existingModels);
410
- log.info(`Loaded ${Object.keys(built).length} plexus models from ${baseURL}`);
411
- } catch (e) {
412
- log.warn(`Live model refresh failed, using cache: ${String(e)}`);
413
- const cached = await readCachedModels(client);
414
- if (cached) {
415
- merged["models"] = mergeModelMaps(cached, existingModels);
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcowger/opencode-plexus",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
4
4
  "description": "OpenCode plugin: Plexus provider with dynamic model discovery",
5
5
  "type": "module",
6
6
  "module": "./dist/index.js",