@mcowger/opencode-plexus 0.8.2 → 0.8.3

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 +81 -55
  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,18 +47,31 @@ 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
77
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
@@ -86,24 +100,6 @@ async function getDir(client) {
86
100
  resolvedDir = fallbackDir();
87
101
  return resolvedDir;
88
102
  }
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
- }
106
- }
107
103
  async function readCachedModels(client) {
108
104
  try {
109
105
  const dir = await getDir(client);
@@ -313,6 +309,19 @@ function buildModels(models, baseURL) {
313
309
 
314
310
  // src/plugin.ts
315
311
  var lastRefresh = null;
312
+ var inFlightRefresh = null;
313
+ function raceWithTimeout(promise, timeoutMs) {
314
+ return new Promise((resolve) => {
315
+ const timer = setTimeout(() => resolve({ status: "timed-out" }), timeoutMs);
316
+ promise.then((value) => {
317
+ clearTimeout(timer);
318
+ resolve({ status: "resolved", value });
319
+ }, (error) => {
320
+ clearTimeout(timer);
321
+ resolve({ status: "rejected", error });
322
+ });
323
+ });
324
+ }
316
325
  function mergeModelMaps(base, overrides) {
317
326
  if (!overrides)
318
327
  return base;
@@ -348,22 +357,30 @@ function mergeModelMaps(base, overrides) {
348
357
  }
349
358
  return merged;
350
359
  }
351
- async function refreshModels(client, baseURL, log, apiKey) {
360
+ function refreshModels(client, baseURL, log, apiKey) {
352
361
  if (lastRefresh && Date.now() - lastRefresh.at < REFRESH_TTL_MS) {
353
362
  log.info(`Using in-memory plexus model cache (${Object.keys(lastRefresh.models).length} models)`);
354
- return lastRefresh.models;
363
+ return Promise.resolve(lastRefresh.models);
355
364
  }
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}`);
363
- }
364
- lastRefresh = { at: Date.now(), models: built };
365
- writeCache(client, built, raw).catch(() => {});
366
- return built;
365
+ if (inFlightRefresh)
366
+ return inFlightRefresh;
367
+ const run = async () => {
368
+ const url = modelsUrl(baseURL);
369
+ const { models: apiModels, raw } = await fetchPlexusModels(apiKey ?? "", url);
370
+ const built = buildModels(apiModels, apiBase(baseURL));
371
+ for (const [id, model] of Object.entries(built)) {
372
+ const providerNpm = model.provider?.npm ?? OPENAI_COMPATIBLE_NPM;
373
+ const providerApi = model.provider?.api ?? "(missing)";
374
+ log.info(`Model mapping ${id}: npm=${providerNpm} api=${providerApi}`);
375
+ }
376
+ lastRefresh = { at: Date.now(), models: built };
377
+ writeCache(client, built, raw).catch(() => {});
378
+ return built;
379
+ };
380
+ inFlightRefresh = run().finally(() => {
381
+ inFlightRefresh = null;
382
+ });
383
+ return inFlightRefresh;
367
384
  }
368
385
  var PlexusProviderPlugin = async (ctx) => {
369
386
  const { client } = ctx;
@@ -379,9 +396,9 @@ var PlexusProviderPlugin = async (ctx) => {
379
396
  if (typeof existingOptions["baseURL"] === "string") {
380
397
  log.warn(`Ignoring legacy provider.options.baseURL=${String(existingOptions["baseURL"])}`);
381
398
  }
382
- const cachedSync = readCachedModelsSync();
383
- if (cachedSync) {
384
- log.info(`Loaded sync plexus cache with ${Object.keys(cachedSync).length} models`);
399
+ const cachedAsync = await readCachedModels(client);
400
+ if (cachedAsync) {
401
+ log.info(`Loaded plexus cache with ${Object.keys(cachedAsync).length} models`);
385
402
  }
386
403
  const merged = {
387
404
  ...existing,
@@ -392,7 +409,7 @@ var PlexusProviderPlugin = async (ctx) => {
392
409
  ...baseURL ? { [PLEXUS_BASE_URL_OPTION]: baseURL } : {},
393
410
  ...apiKey ? { apiKey } : {}
394
411
  },
395
- models: existingModels ?? cachedSync ?? {
412
+ models: existingModels ?? cachedAsync ?? {
396
413
  [PLACEHOLDER_MODEL_ID]: {
397
414
  id: PLACEHOLDER_MODEL_ID,
398
415
  name: "Plexus (run /connect to configure)",
@@ -404,16 +421,24 @@ var PlexusProviderPlugin = async (ctx) => {
404
421
  const mergedOptions = merged["options"];
405
422
  delete mergedOptions["baseURL"];
406
423
  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);
424
+ const refreshPromise = refreshModels(client, baseURL, log, apiKey);
425
+ const race = await raceWithTimeout(refreshPromise, CONFIG_HOOK_REFRESH_BUDGET_MS);
426
+ if (race.status === "resolved") {
427
+ merged["models"] = mergeModelMaps(race.value, existingModels);
428
+ log.info(`Loaded ${Object.keys(race.value).length} plexus models from ${baseURL}`);
429
+ } else if (race.status === "rejected") {
430
+ log.warn(`Live model refresh failed, using cache: ${String(race.error)}`);
431
+ if (cachedAsync) {
432
+ merged["models"] = mergeModelMaps(cachedAsync, existingModels);
433
+ }
434
+ } else {
435
+ log.info(`Live model refresh still pending after ${CONFIG_HOOK_REFRESH_BUDGET_MS}ms; using cache and continuing in background`);
436
+ if (cachedAsync) {
437
+ merged["models"] = mergeModelMaps(cachedAsync, existingModels);
416
438
  }
439
+ refreshPromise.catch((e) => {
440
+ log.warn(`Background plexus model refresh failed: ${String(e)}`);
441
+ });
417
442
  }
418
443
  } else {
419
444
  log.info("Plexus baseURL not configured; skipping live refresh");
@@ -499,5 +524,6 @@ export {
499
524
  OPENAI_COMPATIBLE_NPM,
500
525
  MODELS_FETCH_TIMEOUT_MS,
501
526
  ENV_BASE_URL,
502
- ENV_API_KEY
527
+ ENV_API_KEY,
528
+ CONFIG_HOOK_REFRESH_BUDGET_MS
503
529
  };
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.3",
4
4
  "description": "OpenCode plugin: Plexus provider with dynamic model discovery",
5
5
  "type": "module",
6
6
  "module": "./dist/index.js",