@mcowger/opencode-plexus 0.8.1 → 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 +83 -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
@@ -38,24 +39,39 @@ function mapPreferredApi(raw) {
38
39
  function adjustBaseUrl(baseUrl, preferredApi) {
39
40
  const stripped = baseUrl.replace(/\/+$/, "");
40
41
  switch (preferredApi) {
42
+ case "anthropic-messages":
43
+ return stripped.endsWith("/v1") ? stripped.slice(0, -3) : stripped;
41
44
  case "google-generative-ai":
42
45
  return stripped.endsWith("/v1") ? `${stripped.slice(0, -3)}/v1beta` : stripped;
43
46
  default:
44
47
  return stripped;
45
48
  }
46
49
  }
47
- async function fetchPlexusModels(apiKey, modelsUrl) {
48
- const res = await fetch(modelsUrl, {
49
- headers: {
50
- Authorization: `Bearer ${apiKey}`,
51
- 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}`);
52
64
  }
53
- });
54
- if (!res.ok) {
55
- 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);
56
74
  }
57
- const raw = await res.json();
58
- return { models: raw.data ?? [], raw };
59
75
  }
60
76
  // src/cache.ts
61
77
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
@@ -84,24 +100,6 @@ async function getDir(client) {
84
100
  resolvedDir = fallbackDir();
85
101
  return resolvedDir;
86
102
  }
87
- function syncCachePath() {
88
- return join(fallbackDir(), CACHE_FILE);
89
- }
90
- function readCachedModelsSync() {
91
- try {
92
- const path = syncCachePath();
93
- if (!existsSync(path))
94
- return null;
95
- const raw = readFileSync(path, "utf8");
96
- const parsed = JSON.parse(raw);
97
- if (parsed && typeof parsed.models === "object" && !Array.isArray(parsed.models)) {
98
- return parsed.models;
99
- }
100
- return null;
101
- } catch {
102
- return null;
103
- }
104
- }
105
103
  async function readCachedModels(client) {
106
104
  try {
107
105
  const dir = await getDir(client);
@@ -311,6 +309,19 @@ function buildModels(models, baseURL) {
311
309
 
312
310
  // src/plugin.ts
313
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
+ }
314
325
  function mergeModelMaps(base, overrides) {
315
326
  if (!overrides)
316
327
  return base;
@@ -346,22 +357,30 @@ function mergeModelMaps(base, overrides) {
346
357
  }
347
358
  return merged;
348
359
  }
349
- async function refreshModels(client, baseURL, log, apiKey) {
360
+ function refreshModels(client, baseURL, log, apiKey) {
350
361
  if (lastRefresh && Date.now() - lastRefresh.at < REFRESH_TTL_MS) {
351
362
  log.info(`Using in-memory plexus model cache (${Object.keys(lastRefresh.models).length} models)`);
352
- return lastRefresh.models;
363
+ return Promise.resolve(lastRefresh.models);
353
364
  }
354
- const url = modelsUrl(baseURL);
355
- const { models: apiModels, raw } = await fetchPlexusModels(apiKey ?? "", url);
356
- const built = buildModels(apiModels, apiBase(baseURL));
357
- for (const [id, model] of Object.entries(built)) {
358
- const providerNpm = model.provider?.npm ?? OPENAI_COMPATIBLE_NPM;
359
- const providerApi = model.provider?.api ?? "(missing)";
360
- log.info(`Model mapping ${id}: npm=${providerNpm} api=${providerApi}`);
361
- }
362
- lastRefresh = { at: Date.now(), models: built };
363
- writeCache(client, built, raw).catch(() => {});
364
- 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;
365
384
  }
366
385
  var PlexusProviderPlugin = async (ctx) => {
367
386
  const { client } = ctx;
@@ -377,9 +396,9 @@ var PlexusProviderPlugin = async (ctx) => {
377
396
  if (typeof existingOptions["baseURL"] === "string") {
378
397
  log.warn(`Ignoring legacy provider.options.baseURL=${String(existingOptions["baseURL"])}`);
379
398
  }
380
- const cachedSync = readCachedModelsSync();
381
- if (cachedSync) {
382
- 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`);
383
402
  }
384
403
  const merged = {
385
404
  ...existing,
@@ -390,7 +409,7 @@ var PlexusProviderPlugin = async (ctx) => {
390
409
  ...baseURL ? { [PLEXUS_BASE_URL_OPTION]: baseURL } : {},
391
410
  ...apiKey ? { apiKey } : {}
392
411
  },
393
- models: existingModels ?? cachedSync ?? {
412
+ models: existingModels ?? cachedAsync ?? {
394
413
  [PLACEHOLDER_MODEL_ID]: {
395
414
  id: PLACEHOLDER_MODEL_ID,
396
415
  name: "Plexus (run /connect to configure)",
@@ -402,16 +421,24 @@ var PlexusProviderPlugin = async (ctx) => {
402
421
  const mergedOptions = merged["options"];
403
422
  delete mergedOptions["baseURL"];
404
423
  if (baseURL) {
405
- try {
406
- const built = await refreshModels(client, baseURL, log, apiKey);
407
- merged["models"] = mergeModelMaps(built, existingModels);
408
- log.info(`Loaded ${Object.keys(built).length} plexus models from ${baseURL}`);
409
- } catch (e) {
410
- log.warn(`Live model refresh failed, using cache: ${String(e)}`);
411
- const cached = await readCachedModels(client);
412
- if (cached) {
413
- 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);
414
438
  }
439
+ refreshPromise.catch((e) => {
440
+ log.warn(`Background plexus model refresh failed: ${String(e)}`);
441
+ });
415
442
  }
416
443
  } else {
417
444
  log.info("Plexus baseURL not configured; skipping live refresh");
@@ -497,5 +524,6 @@ export {
497
524
  OPENAI_COMPATIBLE_NPM,
498
525
  MODELS_FETCH_TIMEOUT_MS,
499
526
  ENV_BASE_URL,
500
- ENV_API_KEY
527
+ ENV_API_KEY,
528
+ CONFIG_HOOK_REFRESH_BUDGET_MS
501
529
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcowger/opencode-plexus",
3
- "version": "0.8.1",
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",